@eeacms/volto-eea-chatbot 2.0.1 → 2.0.3

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.
Files changed (84) hide show
  1. package/.eslintrc.js +6 -6
  2. package/CHANGELOG.md +20 -0
  3. package/artifacts/ONYX_V3_INTEGRATION.md +34 -0
  4. package/jest-addon.config.js +2 -1
  5. package/package.json +1 -1
  6. package/src/ChatBlock/ChatBlockEdit.jsx +2 -1
  7. package/src/ChatBlock/chat/AIMessage.tsx +36 -16
  8. package/src/ChatBlock/chat/ChatMessage.tsx +1 -1
  9. package/src/ChatBlock/chat/ChatWindow.tsx +13 -11
  10. package/src/ChatBlock/chat/UserMessage.tsx +4 -4
  11. package/src/ChatBlock/components/AutoResizeTextarea.jsx +1 -1
  12. package/src/ChatBlock/components/ChatMessageFeedback.jsx +2 -2
  13. package/src/ChatBlock/components/EmptyState.jsx +1 -1
  14. package/src/ChatBlock/components/FeedbackModal.jsx +1 -1
  15. package/src/ChatBlock/components/HalloumiFeedback.jsx +2 -2
  16. package/src/ChatBlock/components/Source.jsx +2 -2
  17. package/src/ChatBlock/components/UserActionsToolbar.jsx +3 -3
  18. package/src/ChatBlock/components/WebResultIcon.tsx +2 -2
  19. package/src/ChatBlock/components/markdown/ClaimModal.jsx +3 -3
  20. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +4 -4
  21. package/src/ChatBlock/components/markdown/{index.js → index.jsx} +1 -1
  22. package/src/ChatBlock/hooks/useChatController.ts +67 -14
  23. package/src/ChatBlock/hooks/useChatStreaming.ts +4 -4
  24. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +2 -1
  25. package/src/ChatBlock/packets/MultiToolRenderer.tsx +86 -56
  26. package/src/ChatBlock/packets/RendererComponent.tsx +13 -5
  27. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +3 -3
  28. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +3 -3
  29. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +3 -3
  30. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +14 -9
  31. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +6 -5
  32. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +30 -21
  33. package/src/ChatBlock/{schema.js → schema.jsx} +13 -0
  34. package/src/ChatBlock/services/messageProcessor.ts +72 -17
  35. package/src/ChatBlock/services/packetUtils.ts +13 -3
  36. package/src/ChatBlock/services/streamingService.ts +155 -68
  37. package/src/ChatBlock/types/streamingModels.ts +47 -2
  38. package/src/ChatBlock/utils/citations.ts +1 -1
  39. package/src/halloumi/filtering.test.js +199 -1
  40. package/src/middleware.js +18 -1
  41. package/src/middleware.test.js +14 -0
  42. package/src/ChatBlock/tests/AIMessage.test.jsx +0 -95
  43. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +0 -49
  44. package/src/ChatBlock/tests/BlinkingDot.test.jsx +0 -71
  45. package/src/ChatBlock/tests/ChatMessage.test.jsx +0 -75
  46. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +0 -73
  47. package/src/ChatBlock/tests/Citation.test.jsx +0 -107
  48. package/src/ChatBlock/tests/ClaimModal.test.jsx +0 -136
  49. package/src/ChatBlock/tests/ClaimSegments.test.jsx +0 -206
  50. package/src/ChatBlock/tests/CustomToolRenderer.test.jsx +0 -241
  51. package/src/ChatBlock/tests/EmptyState.test.jsx +0 -137
  52. package/src/ChatBlock/tests/FeedbackModal.test.jsx +0 -138
  53. package/src/ChatBlock/tests/FetchToolRenderer.test.jsx +0 -161
  54. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +0 -94
  55. package/src/ChatBlock/tests/ImageToolRenderer.test.jsx +0 -178
  56. package/src/ChatBlock/tests/MessageTextRenderer.test.jsx +0 -227
  57. package/src/ChatBlock/tests/MultiToolRenderer.test.jsx +0 -134
  58. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +0 -105
  59. package/src/ChatBlock/tests/ReasoningRenderer.test.jsx +0 -163
  60. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +0 -215
  61. package/src/ChatBlock/tests/RenderClaimView.test.jsx +0 -191
  62. package/src/ChatBlock/tests/RendererComponent.test.jsx +0 -139
  63. package/src/ChatBlock/tests/SearchToolRenderer.test.jsx +0 -295
  64. package/src/ChatBlock/tests/Source.test.jsx +0 -79
  65. package/src/ChatBlock/tests/SourceChip.test.jsx +0 -108
  66. package/src/ChatBlock/tests/Spinner.test.jsx +0 -18
  67. package/src/ChatBlock/tests/UserActionsToolbar.test.jsx +0 -135
  68. package/src/ChatBlock/tests/UserMessage.test.jsx +0 -83
  69. package/src/ChatBlock/tests/WebResultIcon.test.jsx +0 -61
  70. package/src/ChatBlock/tests/citations.test.js +0 -114
  71. package/src/ChatBlock/tests/index.test.js +0 -51
  72. package/src/ChatBlock/tests/messageProcessor.test.jsx +0 -438
  73. package/src/ChatBlock/tests/packetUtils.test.js +0 -158
  74. package/src/ChatBlock/tests/schema.test.js +0 -166
  75. package/src/ChatBlock/tests/streamingService.test.js +0 -467
  76. package/src/ChatBlock/tests/useChatController.test.jsx +0 -268
  77. package/src/ChatBlock/tests/useChatStreaming.test.jsx +0 -163
  78. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +0 -107
  79. package/src/ChatBlock/tests/useMarked.test.jsx +0 -107
  80. package/src/ChatBlock/tests/useQualityMarkers.test.jsx +0 -150
  81. package/src/ChatBlock/tests/useScrollonStream.test.jsx +0 -121
  82. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +0 -151
  83. package/src/ChatBlock/tests/utils.test.jsx +0 -241
  84. package/src/ChatBlock/tests/withOnyxData.test.jsx +0 -81
@@ -1,215 +0,0 @@
1
- import React from 'react';
2
- import { render, fireEvent } from '@testing-library/react';
3
- import '@testing-library/jest-dom';
4
- import RelatedQuestions from '../components/RelatedQuestions';
5
-
6
- import { trackEvent } from '@eeacms/volto-matomo/utils';
7
-
8
- // Mock @eeacms/volto-matomo/utils
9
- jest.mock('@eeacms/volto-matomo/utils', () => ({
10
- trackEvent: jest.fn(),
11
- }));
12
-
13
- describe('RelatedQuestions', () => {
14
- const mockOnChoice = jest.fn();
15
-
16
- beforeEach(() => {
17
- jest.clearAllMocks();
18
- });
19
-
20
- it('renders nothing when no related questions', () => {
21
- const { container } = render(
22
- <RelatedQuestions
23
- message={{ relatedQuestions: [] }}
24
- onChoice={mockOnChoice}
25
- isLoading={false}
26
- />,
27
- );
28
- expect(container.querySelector('.chat-related-questions')).toBeNull();
29
- });
30
-
31
- it('renders nothing when relatedQuestions is undefined', () => {
32
- const { container } = render(
33
- <RelatedQuestions
34
- message={{}}
35
- onChoice={mockOnChoice}
36
- isLoading={false}
37
- />,
38
- );
39
- expect(container.querySelector('.chat-related-questions')).toBeNull();
40
- });
41
-
42
- it('renders related questions when available', () => {
43
- const message = {
44
- relatedQuestions: [
45
- { question: 'Question 1' },
46
- { question: 'Question 2' },
47
- ],
48
- };
49
-
50
- const { getByText } = render(
51
- <RelatedQuestions
52
- message={message}
53
- onChoice={mockOnChoice}
54
- isLoading={false}
55
- />,
56
- );
57
-
58
- expect(getByText('Related questions:')).toBeInTheDocument();
59
- expect(getByText('Question 1')).toBeInTheDocument();
60
- expect(getByText('Question 2')).toBeInTheDocument();
61
- });
62
-
63
- it('calls onChoice when question is clicked', () => {
64
- const message = {
65
- relatedQuestions: [{ question: 'Test Question' }],
66
- };
67
-
68
- const { getByText } = render(
69
- <RelatedQuestions
70
- message={message}
71
- onChoice={mockOnChoice}
72
- isLoading={false}
73
- />,
74
- );
75
-
76
- fireEvent.click(getByText('Test Question'));
77
- expect(mockOnChoice).toHaveBeenCalledWith('Test Question');
78
- });
79
-
80
- it('does not call onChoice when loading', () => {
81
- const message = {
82
- relatedQuestions: [{ question: 'Test Question' }],
83
- };
84
-
85
- const { getByText } = render(
86
- <RelatedQuestions
87
- message={message}
88
- onChoice={mockOnChoice}
89
- isLoading={true}
90
- />,
91
- );
92
-
93
- fireEvent.click(getByText('Test Question'));
94
- expect(mockOnChoice).not.toHaveBeenCalled();
95
- });
96
-
97
- it('tracks event when enableMatomoTracking is true', () => {
98
- const message = {
99
- relatedQuestions: [{ question: 'Tracked Question' }],
100
- };
101
- const persona = { name: 'Test Persona' };
102
-
103
- const { getByText } = render(
104
- <RelatedQuestions
105
- message={message}
106
- onChoice={mockOnChoice}
107
- isLoading={false}
108
- enableMatomoTracking={true}
109
- persona={persona}
110
- />,
111
- );
112
-
113
- fireEvent.click(getByText('Tracked Question'));
114
-
115
- expect(trackEvent).toHaveBeenCalledWith({
116
- category: 'Chatbot - Test Persona',
117
- action: 'Chatbot: Related question click',
118
- name: 'Message submitted',
119
- });
120
- });
121
-
122
- it('tracks event with default category when no persona name', () => {
123
- const message = {
124
- relatedQuestions: [{ question: 'Tracked Question' }],
125
- };
126
-
127
- const { getByText } = render(
128
- <RelatedQuestions
129
- message={message}
130
- onChoice={mockOnChoice}
131
- isLoading={false}
132
- enableMatomoTracking={true}
133
- persona={{}}
134
- />,
135
- );
136
-
137
- fireEvent.click(getByText('Tracked Question'));
138
-
139
- expect(trackEvent).toHaveBeenCalledWith({
140
- category: 'Chatbot',
141
- action: 'Chatbot: Related question click',
142
- name: 'Message submitted',
143
- });
144
- });
145
-
146
- it('does not track event when enableMatomoTracking is false', () => {
147
- const message = {
148
- relatedQuestions: [{ question: 'Untracked Question' }],
149
- };
150
-
151
- const { getByText } = render(
152
- <RelatedQuestions
153
- message={message}
154
- onChoice={mockOnChoice}
155
- isLoading={false}
156
- enableMatomoTracking={false}
157
- />,
158
- );
159
-
160
- fireEvent.click(getByText('Untracked Question'));
161
-
162
- expect(trackEvent).not.toHaveBeenCalled();
163
- });
164
-
165
- it('handles keyboard Enter key', () => {
166
- const message = {
167
- relatedQuestions: [{ question: 'Keyboard Question' }],
168
- };
169
-
170
- const { getByText } = render(
171
- <RelatedQuestions
172
- message={message}
173
- onChoice={mockOnChoice}
174
- isLoading={false}
175
- />,
176
- );
177
-
178
- fireEvent.keyDown(getByText('Keyboard Question'), { key: 'Enter' });
179
- expect(mockOnChoice).toHaveBeenCalledWith('Keyboard Question');
180
- });
181
-
182
- it('handles keyboard Space key', () => {
183
- const message = {
184
- relatedQuestions: [{ question: 'Space Question' }],
185
- };
186
-
187
- const { getByText } = render(
188
- <RelatedQuestions
189
- message={message}
190
- onChoice={mockOnChoice}
191
- isLoading={false}
192
- />,
193
- );
194
-
195
- fireEvent.keyDown(getByText('Space Question'), { key: ' ' });
196
- expect(mockOnChoice).toHaveBeenCalledWith('Space Question');
197
- });
198
-
199
- it('ignores other keyboard keys', () => {
200
- const message = {
201
- relatedQuestions: [{ question: 'Other Key Question' }],
202
- };
203
-
204
- const { getByText } = render(
205
- <RelatedQuestions
206
- message={message}
207
- onChoice={mockOnChoice}
208
- isLoading={false}
209
- />,
210
- );
211
-
212
- fireEvent.keyDown(getByText('Other Key Question'), { key: 'Tab' });
213
- expect(mockOnChoice).not.toHaveBeenCalled();
214
- });
215
- });
@@ -1,191 +0,0 @@
1
- import React from 'react';
2
- import renderer from 'react-test-renderer';
3
- import { render, screen } from '@testing-library/react';
4
- import '@testing-library/jest-dom';
5
- import { RenderClaimView } from '../components/markdown/RenderClaimView';
6
-
7
- describe('RenderClaimView', () => {
8
- const createRef = () => ({ current: {} });
9
-
10
- it('renders plain text without segments', () => {
11
- const props = {
12
- value: 'This is plain text without any segments.',
13
- segments: [],
14
- sourceStartIndex: 0,
15
- visibleSegmentId: null,
16
- segmentContainerRef: createRef(),
17
- spanRefs: createRef(),
18
- };
19
-
20
- const component = renderer.create(<RenderClaimView {...props} />);
21
- const json = component.toJSON();
22
- expect(json).toMatchSnapshot();
23
- });
24
-
25
- it('renders text with a single segment', () => {
26
- const props = {
27
- value: 'This is text with a segment here.',
28
- segments: [
29
- {
30
- id: 1,
31
- startOffset: 20,
32
- endOffset: 27,
33
- },
34
- ],
35
- sourceStartIndex: 0,
36
- visibleSegmentId: null,
37
- segmentContainerRef: createRef(),
38
- spanRefs: createRef(),
39
- };
40
-
41
- const component = renderer.create(<RenderClaimView {...props} />);
42
- const json = component.toJSON();
43
- expect(json).toMatchSnapshot();
44
- });
45
-
46
- it('renders text with multiple segments', () => {
47
- const props = {
48
- value: 'First segment and second segment here.',
49
- segments: [
50
- { id: 1, startOffset: 0, endOffset: 13 },
51
- { id: 2, startOffset: 18, endOffset: 32 },
52
- ],
53
- sourceStartIndex: 0,
54
- visibleSegmentId: null,
55
- segmentContainerRef: createRef(),
56
- spanRefs: createRef(),
57
- };
58
-
59
- const component = renderer.create(<RenderClaimView {...props} />);
60
- const json = component.toJSON();
61
- expect(json).toMatchSnapshot();
62
- });
63
-
64
- it('highlights visible segment', () => {
65
- const props = {
66
- value: 'This is text with a segment here.',
67
- segments: [
68
- {
69
- id: 1,
70
- startOffset: 20,
71
- endOffset: 27,
72
- },
73
- ],
74
- sourceStartIndex: 0,
75
- visibleSegmentId: 1,
76
- segmentContainerRef: createRef(),
77
- spanRefs: createRef(),
78
- };
79
-
80
- render(<RenderClaimView {...props} />);
81
- const segment = document.querySelector('.citation-segment.active');
82
- expect(segment).toBeInTheDocument();
83
- });
84
-
85
- it('handles segments with sourceStartIndex offset', () => {
86
- const props = {
87
- value: 'text with segment.',
88
- segments: [
89
- {
90
- id: 1,
91
- startOffset: 110,
92
- endOffset: 117,
93
- },
94
- ],
95
- sourceStartIndex: 100,
96
- visibleSegmentId: null,
97
- segmentContainerRef: createRef(),
98
- spanRefs: createRef(),
99
- };
100
-
101
- const component = renderer.create(<RenderClaimView {...props} />);
102
- const json = component.toJSON();
103
- expect(json).toMatchSnapshot();
104
- });
105
-
106
- it('handles text with newlines', () => {
107
- const props = {
108
- value: 'Line one\nLine two\nLine three',
109
- segments: [],
110
- sourceStartIndex: 0,
111
- visibleSegmentId: null,
112
- segmentContainerRef: createRef(),
113
- spanRefs: createRef(),
114
- };
115
-
116
- const component = renderer.create(<RenderClaimView {...props} />);
117
- const json = component.toJSON();
118
- expect(json).toMatchSnapshot();
119
- });
120
-
121
- it('handles segment ending with newline', () => {
122
- const props = {
123
- value: 'Segment text\nMore text',
124
- segments: [
125
- {
126
- id: 1,
127
- startOffset: 0,
128
- endOffset: 13,
129
- },
130
- ],
131
- sourceStartIndex: 0,
132
- visibleSegmentId: null,
133
- segmentContainerRef: createRef(),
134
- spanRefs: createRef(),
135
- };
136
-
137
- const component = renderer.create(<RenderClaimView {...props} />);
138
- const json = component.toJSON();
139
- expect(json).toMatchSnapshot();
140
- });
141
-
142
- it('filters out DOCUMENT and Source lines', () => {
143
- const props = {
144
- value: 'DOCUMENT 1\nActual content\nSource: test',
145
- segments: [],
146
- sourceStartIndex: 0,
147
- visibleSegmentId: null,
148
- segmentContainerRef: createRef(),
149
- spanRefs: createRef(),
150
- };
151
-
152
- render(<RenderClaimView {...props} />);
153
- expect(screen.queryByText(/DOCUMENT 1/)).not.toBeInTheDocument();
154
- expect(screen.queryByText(/Source: test/)).not.toBeInTheDocument();
155
- expect(screen.getByText('Actual content')).toBeInTheDocument();
156
- });
157
-
158
- it('handles empty segments array', () => {
159
- const props = {
160
- value: 'Some text content',
161
- segments: [],
162
- sourceStartIndex: 0,
163
- visibleSegmentId: null,
164
- segmentContainerRef: createRef(),
165
- spanRefs: createRef(),
166
- };
167
-
168
- const component = renderer.create(<RenderClaimView {...props} />);
169
- const json = component.toJSON();
170
- expect(json).toMatchSnapshot();
171
- });
172
-
173
- it('sorts segments by startOffset', () => {
174
- const props = {
175
- value: 'AAAA BBBB CCCC',
176
- segments: [
177
- { id: 2, startOffset: 5, endOffset: 9 },
178
- { id: 1, startOffset: 0, endOffset: 4 },
179
- { id: 3, startOffset: 10, endOffset: 14 },
180
- ],
181
- sourceStartIndex: 0,
182
- visibleSegmentId: null,
183
- segmentContainerRef: createRef(),
184
- spanRefs: createRef(),
185
- };
186
-
187
- const component = renderer.create(<RenderClaimView {...props} />);
188
- const json = component.toJSON();
189
- expect(json).toMatchSnapshot();
190
- });
191
- });
@@ -1,139 +0,0 @@
1
- import React from 'react';
2
- import renderer from 'react-test-renderer';
3
- import { findRenderer, RendererComponent } from '../packets/RendererComponent';
4
- import { PacketType } from '../types/streamingModels';
5
-
6
- // Mock loadable
7
- jest.mock('@loadable/component', () => {
8
- const loadable = (loader) => {
9
- const MockComponent = (props) => (
10
- <div data-testid="loadable">{props.children}</div>
11
- );
12
- return MockComponent;
13
- };
14
- loadable.lib = () => {
15
- const MockComponent = ({ children }) =>
16
- children ? children({ default: {} }) : null;
17
- return MockComponent;
18
- };
19
- return { __esModule: true, default: loadable };
20
- });
21
-
22
- describe('findRenderer', () => {
23
- it('returns MessageTextRenderer for chat packets', () => {
24
- const result = findRenderer({
25
- packets: [{ ind: 1, obj: { type: PacketType.MESSAGE_START } }],
26
- });
27
- expect(result).toBeDefined();
28
- expect(result.name || result.displayName || '').toBeTruthy();
29
- });
30
-
31
- it('returns MessageTextRenderer for MESSAGE_DELTA', () => {
32
- const result = findRenderer({
33
- packets: [{ ind: 1, obj: { type: PacketType.MESSAGE_DELTA } }],
34
- });
35
- expect(result).toBeDefined();
36
- });
37
-
38
- it('returns MessageTextRenderer for MESSAGE_END', () => {
39
- const result = findRenderer({
40
- packets: [{ ind: 1, obj: { type: PacketType.MESSAGE_END } }],
41
- });
42
- expect(result).toBeDefined();
43
- });
44
-
45
- it('returns SearchToolRenderer for search packets', () => {
46
- const result = findRenderer({
47
- packets: [{ ind: 1, obj: { type: PacketType.SEARCH_TOOL_START } }],
48
- });
49
- expect(result).toBeDefined();
50
- });
51
-
52
- it('returns ImageToolRenderer for image packets', () => {
53
- const result = findRenderer({
54
- packets: [
55
- { ind: 1, obj: { type: PacketType.IMAGE_GENERATION_TOOL_START } },
56
- ],
57
- });
58
- expect(result).toBeDefined();
59
- });
60
-
61
- it('returns CustomToolRenderer for custom tool packets', () => {
62
- const result = findRenderer({
63
- packets: [{ ind: 1, obj: { type: PacketType.CUSTOM_TOOL_START } }],
64
- });
65
- expect(result).toBeDefined();
66
- });
67
-
68
- it('returns FetchToolRenderer for fetch tool packets', () => {
69
- const result = findRenderer({
70
- packets: [{ ind: 1, obj: { type: PacketType.FETCH_TOOL_START } }],
71
- });
72
- expect(result).toBeDefined();
73
- });
74
-
75
- it('returns ReasoningRenderer for reasoning packets', () => {
76
- const result = findRenderer({
77
- packets: [{ ind: 1, obj: { type: PacketType.REASONING_START } }],
78
- });
79
- expect(result).toBeDefined();
80
- });
81
-
82
- it('returns ReasoningRenderer for REASONING_DELTA', () => {
83
- const result = findRenderer({
84
- packets: [{ ind: 1, obj: { type: PacketType.REASONING_DELTA } }],
85
- });
86
- expect(result).toBeDefined();
87
- });
88
-
89
- it('returns null for unknown packet types', () => {
90
- const result = findRenderer({
91
- packets: [{ ind: 1, obj: { type: 'unknown_type' } }],
92
- });
93
- expect(result).toBeNull();
94
- });
95
-
96
- it('returns null for empty packets', () => {
97
- const result = findRenderer({ packets: [] });
98
- expect(result).toBeNull();
99
- });
100
- });
101
-
102
- describe('RendererComponent', () => {
103
- const childRenderer = (result) => (
104
- <div>
105
- <span>{result.status}</span>
106
- <span>{result.content}</span>
107
- </div>
108
- );
109
-
110
- it('renders fallback for unrecognized packets', () => {
111
- const component = renderer.create(
112
- <RendererComponent
113
- packets={[{ ind: 1, obj: { type: 'unknown_type' } }]}
114
- onComplete={jest.fn()}
115
- animate={false}
116
- stopPacketSeen={false}
117
- libs={{ remarkGfm: { default: [] } }}
118
- >
119
- {childRenderer}
120
- </RendererComponent>,
121
- );
122
- expect(component.toJSON()).toMatchSnapshot();
123
- });
124
-
125
- it('renders fallback for empty packets', () => {
126
- const component = renderer.create(
127
- <RendererComponent
128
- packets={[]}
129
- onComplete={jest.fn()}
130
- animate={false}
131
- stopPacketSeen={false}
132
- libs={{ remarkGfm: { default: [] } }}
133
- >
134
- {childRenderer}
135
- </RendererComponent>,
136
- );
137
- expect(component.toJSON()).toMatchSnapshot();
138
- });
139
- });