@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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer, { act } from 'react-test-renderer';
|
|
3
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
4
|
+
import { ImageToolRenderer } from '../packets/renderers/ImageToolRenderer';
|
|
5
|
+
import { PacketType } from '../types/streamingModels';
|
|
6
|
+
|
|
7
|
+
describe('ImageToolRenderer', () => {
|
|
8
|
+
const mockChildRenderer = (result) => (
|
|
9
|
+
<div data-testid="renderer-result">
|
|
10
|
+
<div data-testid="status">{result.status}</div>
|
|
11
|
+
<div data-testid="content">{result.content}</div>
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
it('renders generating state when not complete', () => {
|
|
16
|
+
const props = {
|
|
17
|
+
packets: [
|
|
18
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
19
|
+
],
|
|
20
|
+
onComplete: jest.fn(),
|
|
21
|
+
children: mockChildRenderer,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const component = renderer.create(<ImageToolRenderer {...props} />);
|
|
25
|
+
const json = component.toJSON();
|
|
26
|
+
expect(json).toMatchSnapshot();
|
|
27
|
+
expect(props.onComplete).not.toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('renders complete state with images', () => {
|
|
31
|
+
const props = {
|
|
32
|
+
packets: [
|
|
33
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
34
|
+
{
|
|
35
|
+
ind: 1,
|
|
36
|
+
obj: {
|
|
37
|
+
type: PacketType.IMAGE_GENERATION_TOOL_DELTA,
|
|
38
|
+
images: [
|
|
39
|
+
{
|
|
40
|
+
file_id: 'img1',
|
|
41
|
+
url: 'https://example.com/image1.png',
|
|
42
|
+
revised_prompt: 'A beautiful landscape',
|
|
43
|
+
shape: 'landscape',
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
49
|
+
],
|
|
50
|
+
onComplete: jest.fn(),
|
|
51
|
+
children: mockChildRenderer,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const component = renderer.create(<ImageToolRenderer {...props} />);
|
|
55
|
+
const json = component.toJSON();
|
|
56
|
+
expect(json).toMatchSnapshot();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders multiple images', () => {
|
|
60
|
+
const props = {
|
|
61
|
+
packets: [
|
|
62
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
63
|
+
{
|
|
64
|
+
ind: 1,
|
|
65
|
+
obj: {
|
|
66
|
+
type: PacketType.IMAGE_GENERATION_TOOL_DELTA,
|
|
67
|
+
images: [
|
|
68
|
+
{
|
|
69
|
+
file_id: 'img1',
|
|
70
|
+
url: 'https://example.com/image1.png',
|
|
71
|
+
revised_prompt: 'First image',
|
|
72
|
+
shape: 'square',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
file_id: 'img2',
|
|
76
|
+
url: 'https://example.com/image2.png',
|
|
77
|
+
revised_prompt: 'Second image',
|
|
78
|
+
shape: 'portrait',
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
84
|
+
],
|
|
85
|
+
onComplete: jest.fn(),
|
|
86
|
+
children: mockChildRenderer,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const component = renderer.create(<ImageToolRenderer {...props} />);
|
|
90
|
+
const json = component.toJSON();
|
|
91
|
+
expect(json).toMatchSnapshot();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('handles image without revised_prompt', () => {
|
|
95
|
+
const props = {
|
|
96
|
+
packets: [
|
|
97
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
98
|
+
{
|
|
99
|
+
ind: 1,
|
|
100
|
+
obj: {
|
|
101
|
+
type: PacketType.IMAGE_GENERATION_TOOL_DELTA,
|
|
102
|
+
images: [
|
|
103
|
+
{
|
|
104
|
+
file_id: 'img1',
|
|
105
|
+
url: 'https://example.com/image1.png',
|
|
106
|
+
revised_prompt: '',
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
112
|
+
],
|
|
113
|
+
onComplete: jest.fn(),
|
|
114
|
+
children: mockChildRenderer,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const component = renderer.create(<ImageToolRenderer {...props} />);
|
|
118
|
+
const json = component.toJSON();
|
|
119
|
+
expect(json).toMatchSnapshot();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles image without shape (defaults to square)', () => {
|
|
123
|
+
const props = {
|
|
124
|
+
packets: [
|
|
125
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
126
|
+
{
|
|
127
|
+
ind: 1,
|
|
128
|
+
obj: {
|
|
129
|
+
type: PacketType.IMAGE_GENERATION_TOOL_DELTA,
|
|
130
|
+
images: [
|
|
131
|
+
{
|
|
132
|
+
file_id: 'img1',
|
|
133
|
+
url: 'https://example.com/image1.png',
|
|
134
|
+
revised_prompt: 'Test',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
140
|
+
],
|
|
141
|
+
onComplete: jest.fn(),
|
|
142
|
+
children: mockChildRenderer,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const component = renderer.create(<ImageToolRenderer {...props} />);
|
|
146
|
+
const json = component.toJSON();
|
|
147
|
+
expect(json).toMatchSnapshot();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('handles empty packets array', () => {
|
|
151
|
+
const props = {
|
|
152
|
+
packets: [],
|
|
153
|
+
onComplete: jest.fn(),
|
|
154
|
+
children: mockChildRenderer,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const component = renderer.create(<ImageToolRenderer {...props} />);
|
|
158
|
+
const json = component.toJSON();
|
|
159
|
+
expect(json).toMatchSnapshot();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('calls onComplete when section end is received', () => {
|
|
163
|
+
const onComplete = jest.fn();
|
|
164
|
+
const props = {
|
|
165
|
+
packets: [
|
|
166
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
167
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
168
|
+
],
|
|
169
|
+
onComplete,
|
|
170
|
+
children: mockChildRenderer,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
act(() => {
|
|
174
|
+
renderer.create(<ImageToolRenderer {...props} />);
|
|
175
|
+
});
|
|
176
|
+
expect(onComplete).toHaveBeenCalled();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer, { act } from 'react-test-renderer';
|
|
3
|
+
import { MessageTextRenderer } from '../packets/renderers/MessageTextRenderer';
|
|
4
|
+
import { PacketType } from '../types/streamingModels';
|
|
5
|
+
|
|
6
|
+
// Mock AudioContext
|
|
7
|
+
const mockClose = jest.fn();
|
|
8
|
+
global.AudioContext = jest.fn().mockImplementation(() => ({
|
|
9
|
+
close: mockClose,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock('@loadable/component', () => {
|
|
13
|
+
const loadable = () => {
|
|
14
|
+
const MockMarkdown = ({ children }) => (
|
|
15
|
+
<div data-testid="markdown">{children}</div>
|
|
16
|
+
);
|
|
17
|
+
return MockMarkdown;
|
|
18
|
+
};
|
|
19
|
+
loadable.lib = () => {
|
|
20
|
+
const MockComponent = ({ children }) =>
|
|
21
|
+
children ? children({ default: {} }) : null;
|
|
22
|
+
return MockComponent;
|
|
23
|
+
};
|
|
24
|
+
return { __esModule: true, default: loadable };
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('MessageTextRenderer', () => {
|
|
28
|
+
const mockChildRenderer = (result) => (
|
|
29
|
+
<div data-testid="renderer-result">
|
|
30
|
+
<div data-testid="status">{result.status}</div>
|
|
31
|
+
<div data-testid="content">{result.content}</div>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const defaultMessage = {
|
|
36
|
+
messageId: 1,
|
|
37
|
+
nodeId: 1,
|
|
38
|
+
message: 'Hello world',
|
|
39
|
+
type: 'assistant',
|
|
40
|
+
packets: [],
|
|
41
|
+
files: [],
|
|
42
|
+
toolCall: null,
|
|
43
|
+
parentNodeId: null,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const defaultLibs = { remarkGfm: { default: [] } };
|
|
47
|
+
|
|
48
|
+
it('renders message content without animation', () => {
|
|
49
|
+
const packets = [
|
|
50
|
+
{
|
|
51
|
+
ind: 1,
|
|
52
|
+
obj: {
|
|
53
|
+
type: PacketType.MESSAGE_START,
|
|
54
|
+
content: 'Hello',
|
|
55
|
+
final_documents: null,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{ ind: 1, obj: { type: PacketType.MESSAGE_DELTA, content: ' world' } },
|
|
59
|
+
{ ind: 1, obj: { type: PacketType.MESSAGE_END } },
|
|
60
|
+
{ ind: 1, obj: { type: PacketType.STOP } },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
let component;
|
|
64
|
+
act(() => {
|
|
65
|
+
component = renderer.create(
|
|
66
|
+
<MessageTextRenderer
|
|
67
|
+
packets={packets}
|
|
68
|
+
onComplete={jest.fn()}
|
|
69
|
+
animate={false}
|
|
70
|
+
stopPacketSeen={true}
|
|
71
|
+
message={defaultMessage}
|
|
72
|
+
libs={defaultLibs}
|
|
73
|
+
>
|
|
74
|
+
{mockChildRenderer}
|
|
75
|
+
</MessageTextRenderer>,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('renders blinking dot when no content and no packets', () => {
|
|
82
|
+
const emptyMessage = { ...defaultMessage, message: '' };
|
|
83
|
+
|
|
84
|
+
let component;
|
|
85
|
+
act(() => {
|
|
86
|
+
component = renderer.create(
|
|
87
|
+
<MessageTextRenderer
|
|
88
|
+
packets={[]}
|
|
89
|
+
onComplete={jest.fn()}
|
|
90
|
+
animate={false}
|
|
91
|
+
stopPacketSeen={false}
|
|
92
|
+
message={emptyMessage}
|
|
93
|
+
libs={defaultLibs}
|
|
94
|
+
>
|
|
95
|
+
{mockChildRenderer}
|
|
96
|
+
</MessageTextRenderer>,
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('calls onComplete when stream is finished', () => {
|
|
103
|
+
const onComplete = jest.fn();
|
|
104
|
+
const packets = [
|
|
105
|
+
{
|
|
106
|
+
ind: 1,
|
|
107
|
+
obj: {
|
|
108
|
+
type: PacketType.MESSAGE_START,
|
|
109
|
+
content: 'Hi',
|
|
110
|
+
final_documents: null,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{ ind: 1, obj: { type: PacketType.MESSAGE_END } },
|
|
114
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
renderer.create(
|
|
119
|
+
<MessageTextRenderer
|
|
120
|
+
packets={packets}
|
|
121
|
+
onComplete={onComplete}
|
|
122
|
+
animate={false}
|
|
123
|
+
stopPacketSeen={true}
|
|
124
|
+
message={defaultMessage}
|
|
125
|
+
libs={defaultLibs}
|
|
126
|
+
>
|
|
127
|
+
{mockChildRenderer}
|
|
128
|
+
</MessageTextRenderer>,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(onComplete).toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('shows cursor when stream is not complete', () => {
|
|
136
|
+
const packets = [
|
|
137
|
+
{
|
|
138
|
+
ind: 1,
|
|
139
|
+
obj: {
|
|
140
|
+
type: PacketType.MESSAGE_START,
|
|
141
|
+
content: 'Starting...',
|
|
142
|
+
final_documents: null,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
let component;
|
|
148
|
+
act(() => {
|
|
149
|
+
component = renderer.create(
|
|
150
|
+
<MessageTextRenderer
|
|
151
|
+
packets={packets}
|
|
152
|
+
onComplete={jest.fn()}
|
|
153
|
+
animate={false}
|
|
154
|
+
stopPacketSeen={false}
|
|
155
|
+
message={{ ...defaultMessage, message: 'Starting...' }}
|
|
156
|
+
libs={defaultLibs}
|
|
157
|
+
>
|
|
158
|
+
{mockChildRenderer}
|
|
159
|
+
</MessageTextRenderer>,
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('renders with remarkGfm plugin when available', () => {
|
|
166
|
+
const packets = [
|
|
167
|
+
{
|
|
168
|
+
ind: 1,
|
|
169
|
+
obj: {
|
|
170
|
+
type: PacketType.MESSAGE_START,
|
|
171
|
+
content: 'test',
|
|
172
|
+
final_documents: null,
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{ ind: 1, obj: { type: PacketType.MESSAGE_END } },
|
|
176
|
+
{ ind: 1, obj: { type: PacketType.STOP } },
|
|
177
|
+
];
|
|
178
|
+
|
|
179
|
+
let component;
|
|
180
|
+
act(() => {
|
|
181
|
+
component = renderer.create(
|
|
182
|
+
<MessageTextRenderer
|
|
183
|
+
packets={packets}
|
|
184
|
+
onComplete={jest.fn()}
|
|
185
|
+
animate={false}
|
|
186
|
+
stopPacketSeen={true}
|
|
187
|
+
message={defaultMessage}
|
|
188
|
+
libs={{ remarkGfm: { default: () => {} } }}
|
|
189
|
+
>
|
|
190
|
+
{mockChildRenderer}
|
|
191
|
+
</MessageTextRenderer>,
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('renders with no remarkGfm', () => {
|
|
198
|
+
const packets = [
|
|
199
|
+
{
|
|
200
|
+
ind: 1,
|
|
201
|
+
obj: {
|
|
202
|
+
type: PacketType.MESSAGE_START,
|
|
203
|
+
content: 'test',
|
|
204
|
+
final_documents: null,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{ ind: 1, obj: { type: PacketType.STOP } },
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
let component;
|
|
211
|
+
act(() => {
|
|
212
|
+
component = renderer.create(
|
|
213
|
+
<MessageTextRenderer
|
|
214
|
+
packets={packets}
|
|
215
|
+
onComplete={jest.fn()}
|
|
216
|
+
animate={false}
|
|
217
|
+
stopPacketSeen={true}
|
|
218
|
+
message={defaultMessage}
|
|
219
|
+
libs={{ remarkGfm: null }}
|
|
220
|
+
>
|
|
221
|
+
{mockChildRenderer}
|
|
222
|
+
</MessageTextRenderer>,
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer, { act } from 'react-test-renderer';
|
|
3
|
+
import { MultiToolRenderer } from '../packets/MultiToolRenderer';
|
|
4
|
+
import { PacketType } from '../types/streamingModels';
|
|
5
|
+
|
|
6
|
+
jest.mock('@loadable/component', () => {
|
|
7
|
+
const loadable = () => {
|
|
8
|
+
const MockComponent = ({ children }) => (
|
|
9
|
+
<div data-testid="loadable">{children}</div>
|
|
10
|
+
);
|
|
11
|
+
return MockComponent;
|
|
12
|
+
};
|
|
13
|
+
loadable.lib = () => {
|
|
14
|
+
const MockComponent = ({ children }) =>
|
|
15
|
+
children ? children({ default: {} }) : null;
|
|
16
|
+
return MockComponent;
|
|
17
|
+
};
|
|
18
|
+
return { __esModule: true, default: loadable };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Mock SVGIcon
|
|
22
|
+
jest.mock('../components/Icon', () => {
|
|
23
|
+
return ({ name, size }) => <span data-icon={name} data-size={size} />;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Mock SVG imports
|
|
27
|
+
jest.mock('../../icons/done.svg', () => 'done-icon');
|
|
28
|
+
jest.mock('../../icons/chevron.svg', () => 'chevron-icon');
|
|
29
|
+
|
|
30
|
+
describe('MultiToolRenderer', () => {
|
|
31
|
+
const defaultMessage = {
|
|
32
|
+
messageId: 1,
|
|
33
|
+
nodeId: 1,
|
|
34
|
+
message: 'test',
|
|
35
|
+
type: 'assistant',
|
|
36
|
+
packets: [],
|
|
37
|
+
files: [],
|
|
38
|
+
toolCall: null,
|
|
39
|
+
parentNodeId: null,
|
|
40
|
+
isFinalMessageComing: false,
|
|
41
|
+
isComplete: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const defaultLibs = { remarkGfm: { default: [] } };
|
|
45
|
+
|
|
46
|
+
it('returns null when no tool groups match showTools filter', () => {
|
|
47
|
+
const toolGroups = [
|
|
48
|
+
{
|
|
49
|
+
ind: 1,
|
|
50
|
+
packets: [
|
|
51
|
+
{ ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const component = renderer.create(
|
|
57
|
+
<MultiToolRenderer
|
|
58
|
+
toolGroups={toolGroups}
|
|
59
|
+
showTools={[PacketType.SEARCH_TOOL_START]}
|
|
60
|
+
message={defaultMessage}
|
|
61
|
+
libs={defaultLibs}
|
|
62
|
+
/>,
|
|
63
|
+
);
|
|
64
|
+
expect(component.toJSON()).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns null for empty tool groups', () => {
|
|
68
|
+
const component = renderer.create(
|
|
69
|
+
<MultiToolRenderer
|
|
70
|
+
toolGroups={[]}
|
|
71
|
+
message={defaultMessage}
|
|
72
|
+
libs={defaultLibs}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
expect(component.toJSON()).toBeNull();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('renders search tool groups', () => {
|
|
79
|
+
const toolGroups = [
|
|
80
|
+
{
|
|
81
|
+
ind: 1,
|
|
82
|
+
packets: [
|
|
83
|
+
{ ind: 1, obj: { type: PacketType.SEARCH_TOOL_START } },
|
|
84
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
let component;
|
|
90
|
+
act(() => {
|
|
91
|
+
component = renderer.create(
|
|
92
|
+
<MultiToolRenderer
|
|
93
|
+
toolGroups={toolGroups}
|
|
94
|
+
showTools={[PacketType.SEARCH_TOOL_START]}
|
|
95
|
+
message={defaultMessage}
|
|
96
|
+
libs={defaultLibs}
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('renders multiple tool groups', () => {
|
|
104
|
+
const toolGroups = [
|
|
105
|
+
{
|
|
106
|
+
ind: 1,
|
|
107
|
+
packets: [
|
|
108
|
+
{ ind: 1, obj: { type: PacketType.SEARCH_TOOL_START } },
|
|
109
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
ind: 2,
|
|
114
|
+
packets: [
|
|
115
|
+
{ ind: 2, obj: { type: PacketType.SEARCH_TOOL_START } },
|
|
116
|
+
{ ind: 2, obj: { type: PacketType.SECTION_END } },
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
let component;
|
|
122
|
+
act(() => {
|
|
123
|
+
component = renderer.create(
|
|
124
|
+
<MultiToolRenderer
|
|
125
|
+
toolGroups={toolGroups}
|
|
126
|
+
showTools={[PacketType.SEARCH_TOOL_START]}
|
|
127
|
+
message={defaultMessage}
|
|
128
|
+
libs={defaultLibs}
|
|
129
|
+
/>,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import renderer, { act } from 'react-test-renderer';
|
|
3
|
+
import { ReasoningRenderer } from '../packets/renderers/ReasoningRenderer';
|
|
4
|
+
import { PacketType } from '../types/streamingModels';
|
|
5
|
+
|
|
6
|
+
jest.mock('@loadable/component', () => {
|
|
7
|
+
const loadable = () => {
|
|
8
|
+
const MockMarkdown = ({ children }) => (
|
|
9
|
+
<div data-testid="markdown">{children}</div>
|
|
10
|
+
);
|
|
11
|
+
return MockMarkdown;
|
|
12
|
+
};
|
|
13
|
+
loadable.lib = () => {
|
|
14
|
+
const MockComponent = ({ children }) =>
|
|
15
|
+
children ? children({ default: {} }) : null;
|
|
16
|
+
return MockComponent;
|
|
17
|
+
};
|
|
18
|
+
return { __esModule: true, default: loadable };
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('ReasoningRenderer', () => {
|
|
22
|
+
const mockChildRenderer = (result) => (
|
|
23
|
+
<div data-testid="renderer-result">
|
|
24
|
+
<div data-testid="status">{result.status}</div>
|
|
25
|
+
<div data-testid="content">{result.content}</div>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const defaultMessage = {
|
|
30
|
+
messageId: 1,
|
|
31
|
+
nodeId: 1,
|
|
32
|
+
message: 'test',
|
|
33
|
+
type: 'assistant',
|
|
34
|
+
packets: [],
|
|
35
|
+
files: [],
|
|
36
|
+
toolCall: null,
|
|
37
|
+
parentNodeId: null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const defaultLibs = { remarkGfm: { default: [] } };
|
|
41
|
+
|
|
42
|
+
it('renders empty content when no packets', () => {
|
|
43
|
+
const component = renderer.create(
|
|
44
|
+
<ReasoningRenderer
|
|
45
|
+
packets={[]}
|
|
46
|
+
onComplete={jest.fn()}
|
|
47
|
+
animate={false}
|
|
48
|
+
stopPacketSeen={false}
|
|
49
|
+
message={defaultMessage}
|
|
50
|
+
libs={defaultLibs}
|
|
51
|
+
>
|
|
52
|
+
{mockChildRenderer}
|
|
53
|
+
</ReasoningRenderer>,
|
|
54
|
+
);
|
|
55
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('renders thinking status when reasoning has started', () => {
|
|
59
|
+
const packets = [
|
|
60
|
+
{ ind: 1, obj: { type: PacketType.REASONING_START } },
|
|
61
|
+
{
|
|
62
|
+
ind: 1,
|
|
63
|
+
obj: { type: PacketType.REASONING_DELTA, reasoning: 'Let me think...' },
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const component = renderer.create(
|
|
68
|
+
<ReasoningRenderer
|
|
69
|
+
packets={packets}
|
|
70
|
+
onComplete={jest.fn()}
|
|
71
|
+
animate={false}
|
|
72
|
+
stopPacketSeen={false}
|
|
73
|
+
message={defaultMessage}
|
|
74
|
+
libs={defaultLibs}
|
|
75
|
+
>
|
|
76
|
+
{mockChildRenderer}
|
|
77
|
+
</ReasoningRenderer>,
|
|
78
|
+
);
|
|
79
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('calls onComplete when reasoning ends without animation', () => {
|
|
83
|
+
const onComplete = jest.fn();
|
|
84
|
+
const packets = [
|
|
85
|
+
{ ind: 1, obj: { type: PacketType.REASONING_START } },
|
|
86
|
+
{
|
|
87
|
+
ind: 1,
|
|
88
|
+
obj: { type: PacketType.REASONING_DELTA, reasoning: 'Thinking...' },
|
|
89
|
+
},
|
|
90
|
+
{ ind: 1, obj: { type: PacketType.SECTION_END } },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
act(() => {
|
|
94
|
+
renderer.create(
|
|
95
|
+
<ReasoningRenderer
|
|
96
|
+
packets={packets}
|
|
97
|
+
onComplete={onComplete}
|
|
98
|
+
animate={false}
|
|
99
|
+
stopPacketSeen={false}
|
|
100
|
+
message={defaultMessage}
|
|
101
|
+
libs={defaultLibs}
|
|
102
|
+
>
|
|
103
|
+
{mockChildRenderer}
|
|
104
|
+
</ReasoningRenderer>,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(onComplete).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('renders with REASONING_END packet type', () => {
|
|
112
|
+
const packets = [
|
|
113
|
+
{ ind: 1, obj: { type: PacketType.REASONING_START } },
|
|
114
|
+
{
|
|
115
|
+
ind: 1,
|
|
116
|
+
obj: { type: PacketType.REASONING_DELTA, reasoning: 'Analysis' },
|
|
117
|
+
},
|
|
118
|
+
{ ind: 1, obj: { type: PacketType.REASONING_END } },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const component = renderer.create(
|
|
122
|
+
<ReasoningRenderer
|
|
123
|
+
packets={packets}
|
|
124
|
+
onComplete={jest.fn()}
|
|
125
|
+
animate={false}
|
|
126
|
+
stopPacketSeen={false}
|
|
127
|
+
message={defaultMessage}
|
|
128
|
+
libs={defaultLibs}
|
|
129
|
+
>
|
|
130
|
+
{mockChildRenderer}
|
|
131
|
+
</ReasoningRenderer>,
|
|
132
|
+
);
|
|
133
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('concatenates multiple reasoning deltas', () => {
|
|
137
|
+
const packets = [
|
|
138
|
+
{ ind: 1, obj: { type: PacketType.REASONING_START } },
|
|
139
|
+
{
|
|
140
|
+
ind: 1,
|
|
141
|
+
obj: { type: PacketType.REASONING_DELTA, reasoning: 'First ' },
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
ind: 1,
|
|
145
|
+
obj: { type: PacketType.REASONING_DELTA, reasoning: 'second' },
|
|
146
|
+
},
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const component = renderer.create(
|
|
150
|
+
<ReasoningRenderer
|
|
151
|
+
packets={packets}
|
|
152
|
+
onComplete={jest.fn()}
|
|
153
|
+
animate={false}
|
|
154
|
+
stopPacketSeen={false}
|
|
155
|
+
message={defaultMessage}
|
|
156
|
+
libs={defaultLibs}
|
|
157
|
+
>
|
|
158
|
+
{mockChildRenderer}
|
|
159
|
+
</ReasoningRenderer>,
|
|
160
|
+
);
|
|
161
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
162
|
+
});
|
|
163
|
+
});
|