@eeacms/volto-eea-chatbot 1.0.9
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/.coverage.babel.config.js +9 -0
- package/.eslintrc.js +68 -0
- package/.husky/pre-commit +2 -0
- package/.release-it.json +17 -0
- package/AGENTS.md +89 -0
- package/CHANGELOG.md +770 -0
- package/DEVELOP.md +124 -0
- package/LICENSE.md +9 -0
- package/README.md +170 -0
- package/RELEASE.md +74 -0
- package/TESTING.md +5 -0
- package/babel.config.js +17 -0
- package/bootstrap +41 -0
- package/cypress.config.js +27 -0
- package/docker-compose.yml +32 -0
- package/jest-addon.config.js +465 -0
- package/jest.setup.js +65 -0
- package/locales/de/LC_MESSAGES/volto.po +14 -0
- package/locales/en/LC_MESSAGES/volto.po +14 -0
- package/locales/it/LC_MESSAGES/volto.po +14 -0
- package/locales/ro/LC_MESSAGES/volto.po +14 -0
- package/locales/volto.pot +16 -0
- package/package.json +98 -0
- package/razzle.extend.js +40 -0
- package/src/ChatBlock/ChatBlockEdit.jsx +46 -0
- package/src/ChatBlock/ChatBlockView.jsx +21 -0
- package/src/ChatBlock/chat/AIMessage.tsx +566 -0
- package/src/ChatBlock/chat/ChatMessage.tsx +35 -0
- package/src/ChatBlock/chat/ChatWindow.tsx +288 -0
- package/src/ChatBlock/chat/UserMessage.tsx +27 -0
- package/src/ChatBlock/chat/index.ts +4 -0
- package/src/ChatBlock/components/AutoResizeTextarea.jsx +67 -0
- package/src/ChatBlock/components/BlinkingDot.tsx +3 -0
- package/src/ChatBlock/components/ChatMessageFeedback.jsx +77 -0
- package/src/ChatBlock/components/EmptyState.jsx +70 -0
- package/src/ChatBlock/components/FeedbackModal.jsx +125 -0
- package/src/ChatBlock/components/HalloumiFeedback.jsx +126 -0
- package/src/ChatBlock/components/Icon.tsx +35 -0
- package/src/ChatBlock/components/QualityCheckToggle.jsx +26 -0
- package/src/ChatBlock/components/RelatedQuestions.jsx +59 -0
- package/src/ChatBlock/components/Source.jsx +93 -0
- package/src/ChatBlock/components/SourceChip.tsx +55 -0
- package/src/ChatBlock/components/Spinner.jsx +3 -0
- package/src/ChatBlock/components/UserActionsToolbar.jsx +44 -0
- package/src/ChatBlock/components/WebResultIcon.tsx +42 -0
- package/src/ChatBlock/components/markdown/Citation.jsx +70 -0
- package/src/ChatBlock/components/markdown/ClaimModal.jsx +98 -0
- package/src/ChatBlock/components/markdown/ClaimSegments.jsx +172 -0
- package/src/ChatBlock/components/markdown/RenderClaimView.jsx +96 -0
- package/src/ChatBlock/components/markdown/colors.js +29 -0
- package/src/ChatBlock/components/markdown/colors.less +52 -0
- package/src/ChatBlock/components/markdown/colors.test.js +69 -0
- package/src/ChatBlock/components/markdown/index.js +115 -0
- package/src/ChatBlock/fonts/DejaVuSans.ttf +0 -0
- package/src/ChatBlock/hocs/withOnyxData.jsx +46 -0
- package/src/ChatBlock/hooks/index.ts +7 -0
- package/src/ChatBlock/hooks/useChatController.ts +333 -0
- package/src/ChatBlock/hooks/useChatStreaming.ts +82 -0
- package/src/ChatBlock/hooks/useDeepCompareMemoize.js +17 -0
- package/src/ChatBlock/hooks/useMarked.js +44 -0
- package/src/ChatBlock/hooks/useQualityMarkers.js +119 -0
- package/src/ChatBlock/hooks/useScrollonStream.ts +131 -0
- package/src/ChatBlock/hooks/useToolDisplayTiming.ts +80 -0
- package/src/ChatBlock/index.js +32 -0
- package/src/ChatBlock/packets/MultiToolRenderer.tsx +235 -0
- package/src/ChatBlock/packets/RendererComponent.tsx +115 -0
- package/src/ChatBlock/packets/index.ts +4 -0
- package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +63 -0
- package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +59 -0
- package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +62 -0
- package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +172 -0
- package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +122 -0
- package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +323 -0
- package/src/ChatBlock/packets/renderers/index.ts +6 -0
- package/src/ChatBlock/schema.js +403 -0
- package/src/ChatBlock/services/index.ts +3 -0
- package/src/ChatBlock/services/messageProcessor.ts +348 -0
- package/src/ChatBlock/services/packetUtils.ts +48 -0
- package/src/ChatBlock/services/streamingService.ts +342 -0
- package/src/ChatBlock/style.less +1881 -0
- package/src/ChatBlock/tests/AIMessage.test.jsx +95 -0
- package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +49 -0
- package/src/ChatBlock/tests/BlinkingDot.test.jsx +71 -0
- package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +73 -0
- package/src/ChatBlock/tests/Citation.test.jsx +107 -0
- package/src/ChatBlock/tests/EmptyState.test.jsx +137 -0
- package/src/ChatBlock/tests/FeedbackModal.test.jsx +138 -0
- package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +94 -0
- package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +105 -0
- package/src/ChatBlock/tests/RelatedQuestions.test.jsx +215 -0
- package/src/ChatBlock/tests/Source.test.jsx +79 -0
- package/src/ChatBlock/tests/Spinner.test.jsx +18 -0
- package/src/ChatBlock/tests/index.test.js +51 -0
- package/src/ChatBlock/tests/messageProcessor.test.jsx +154 -0
- package/src/ChatBlock/tests/schema.test.js +166 -0
- package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +107 -0
- package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +151 -0
- package/src/ChatBlock/types/cssmodules.d.ts +7 -0
- package/src/ChatBlock/types/interfaces.ts +154 -0
- package/src/ChatBlock/types/slate.d.ts +3 -0
- package/src/ChatBlock/types/streamingModels.ts +267 -0
- package/src/ChatBlock/types/volto.d.ts +3 -0
- package/src/ChatBlock/utils/citations.ts +25 -0
- package/src/ChatBlock/utils/index.tsx +114 -0
- package/src/halloumi/README.md +1 -0
- package/src/halloumi/generative.js +219 -0
- package/src/halloumi/generative.test.js +88 -0
- package/src/halloumi/middleware.js +70 -0
- package/src/halloumi/postprocessing.js +273 -0
- package/src/halloumi/postprocessing.test.js +441 -0
- package/src/halloumi/preprocessing.js +115 -0
- package/src/halloumi/preprocessing.test.js +245 -0
- package/src/icons/bot.svg +1 -0
- package/src/icons/check.svg +1 -0
- package/src/icons/chevron.svg +3 -0
- package/src/icons/clear.svg +1 -0
- package/src/icons/copy.svg +1 -0
- package/src/icons/done.svg +5 -0
- package/src/icons/external-link.svg +1 -0
- package/src/icons/file.svg +1 -0
- package/src/icons/glasses.svg +1 -0
- package/src/icons/globe.svg +1 -0
- package/src/icons/rotate.svg +1 -0
- package/src/icons/search.svg +5 -0
- package/src/icons/send.svg +1 -0
- package/src/icons/square-pen.svg +1 -0
- package/src/icons/stop.svg +9 -0
- package/src/icons/thumbs-down.svg +1 -0
- package/src/icons/thumbs-up.svg +1 -0
- package/src/icons/user.svg +1 -0
- package/src/index.js +58 -0
- package/src/middleware.js +250 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,95 @@
|
|
|
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 { AIMessage } from '../chat/AIMessage';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
describe('AIMessage', () => {
|
|
12
|
+
let store;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
store = mockStore({
|
|
16
|
+
userSession: { token: '1234' },
|
|
17
|
+
intl: { locale: 'en', messages: {} },
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const renderComponent = (props) =>
|
|
22
|
+
renderer.create(
|
|
23
|
+
<Provider store={store}>
|
|
24
|
+
<MemoryRouter>
|
|
25
|
+
<AIMessage {...props} />
|
|
26
|
+
</MemoryRouter>
|
|
27
|
+
</Provider>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it('renders AI message with content', () => {
|
|
31
|
+
const props = {
|
|
32
|
+
message: {
|
|
33
|
+
messageId: 1,
|
|
34
|
+
message: 'Hello, I am an AI assistant',
|
|
35
|
+
type: 'assistant',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const component = renderComponent(props);
|
|
40
|
+
const json = component.toJSON();
|
|
41
|
+
expect(json).toMatchSnapshot();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders AI message with sources', () => {
|
|
45
|
+
const props = {
|
|
46
|
+
message: {
|
|
47
|
+
messageId: 1,
|
|
48
|
+
message: 'Here is some information',
|
|
49
|
+
type: 'assistant',
|
|
50
|
+
documents: [
|
|
51
|
+
{
|
|
52
|
+
document_id: 'doc1',
|
|
53
|
+
semantic_identifier: 'Source 1',
|
|
54
|
+
link: 'https://example.com/1',
|
|
55
|
+
source_type: 'web',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const component = renderComponent(props);
|
|
62
|
+
const json = component.toJSON();
|
|
63
|
+
expect(json).toMatchSnapshot();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('renders AI message with feedback options', () => {
|
|
67
|
+
const props = {
|
|
68
|
+
message: {
|
|
69
|
+
messageId: 1,
|
|
70
|
+
message: 'This is a response',
|
|
71
|
+
type: 'assistant',
|
|
72
|
+
},
|
|
73
|
+
onFeedback: jest.fn(),
|
|
74
|
+
enableFeedback: true,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const component = renderComponent(props);
|
|
78
|
+
const json = component.toJSON();
|
|
79
|
+
expect(json).toMatchSnapshot();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders empty AI message', () => {
|
|
83
|
+
const props = {
|
|
84
|
+
message: {
|
|
85
|
+
messageId: 1,
|
|
86
|
+
message: '',
|
|
87
|
+
type: 'assistant',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const component = renderComponent(props);
|
|
92
|
+
const json = component.toJSON();
|
|
93
|
+
expect(json).toMatchSnapshot();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom';
|
|
3
|
+
import AutoResizeTextarea from '../components/AutoResizeTextarea';
|
|
4
|
+
|
|
5
|
+
jest.mock('@eeacms/volto-matomo/utils', () => ({
|
|
6
|
+
trackEvent: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
describe('AutoResizeTextarea', () => {
|
|
10
|
+
it('renders textarea and button', () => {
|
|
11
|
+
const { getByRole, getByLabelText } = render(
|
|
12
|
+
<AutoResizeTextarea onSubmit={jest.fn()} />,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
expect(getByRole('textbox')).toBeInTheDocument();
|
|
16
|
+
expect(getByLabelText('Send')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('calls onSubmit with input text on Enter key press', () => {
|
|
20
|
+
const mockSubmit = jest.fn();
|
|
21
|
+
const { getByRole } = render(<AutoResizeTextarea onSubmit={mockSubmit} />);
|
|
22
|
+
const textarea = getByRole('textbox');
|
|
23
|
+
|
|
24
|
+
fireEvent.change(textarea, { target: { value: 'Hello' } });
|
|
25
|
+
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter' });
|
|
26
|
+
|
|
27
|
+
expect(mockSubmit).toHaveBeenCalledWith({ message: 'Hello' });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does not call onSubmit if input is empty', () => {
|
|
31
|
+
const mockSubmit = jest.fn();
|
|
32
|
+
const { getByRole } = render(<AutoResizeTextarea onSubmit={mockSubmit} />);
|
|
33
|
+
const textarea = getByRole('textbox');
|
|
34
|
+
|
|
35
|
+
fireEvent.keyDown(textarea, { key: 'Enter', code: 'Enter' });
|
|
36
|
+
|
|
37
|
+
expect(mockSubmit).not.toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('adds newline on Shift+Enter', () => {
|
|
41
|
+
const { getByRole } = render(<AutoResizeTextarea onSubmit={jest.fn()} />);
|
|
42
|
+
const textarea = getByRole('textbox');
|
|
43
|
+
|
|
44
|
+
fireEvent.change(textarea, { target: { value: 'Line 1' } });
|
|
45
|
+
fireEvent.keyDown(textarea, { key: 'Enter', shiftKey: true });
|
|
46
|
+
|
|
47
|
+
expect(textarea.value).toBe('Line 1\n');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
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 { BlinkingDot } from '../components/BlinkingDot';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
describe('BlinkingDot', () => {
|
|
12
|
+
let store;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
store = mockStore({
|
|
16
|
+
userSession: { token: '1234' },
|
|
17
|
+
intl: { locale: 'en', messages: {} },
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const renderComponent = (props) =>
|
|
22
|
+
renderer.create(
|
|
23
|
+
<Provider store={store}>
|
|
24
|
+
<MemoryRouter>
|
|
25
|
+
<BlinkingDot {...props} />
|
|
26
|
+
</MemoryRouter>
|
|
27
|
+
</Provider>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
it('renders blinking dot when active', () => {
|
|
31
|
+
const props = {
|
|
32
|
+
isActive: true,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const component = renderComponent(props);
|
|
36
|
+
const json = component.toJSON();
|
|
37
|
+
expect(json).toMatchSnapshot();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('renders inactive dot when not active', () => {
|
|
41
|
+
const props = {
|
|
42
|
+
isActive: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const component = renderComponent(props);
|
|
46
|
+
const json = component.toJSON();
|
|
47
|
+
expect(json).toMatchSnapshot();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('applies custom className', () => {
|
|
51
|
+
const props = {
|
|
52
|
+
isActive: true,
|
|
53
|
+
className: 'custom-class',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const component = renderComponent(props);
|
|
57
|
+
const json = component.toJSON();
|
|
58
|
+
expect(json).toMatchSnapshot();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('applies custom size', () => {
|
|
62
|
+
const props = {
|
|
63
|
+
isActive: true,
|
|
64
|
+
size: 20,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const component = renderComponent(props);
|
|
68
|
+
const json = component.toJSON();
|
|
69
|
+
expect(json).toMatchSnapshot();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import ChatMessageFeedback from '../components/ChatMessageFeedback';
|
|
4
|
+
|
|
5
|
+
jest.mock('../components/FeedbackModal', () => (props) => {
|
|
6
|
+
const { modalOpen, onClose, onToast } = props;
|
|
7
|
+
|
|
8
|
+
return modalOpen ? (
|
|
9
|
+
<div data-testid="feedback-modal">
|
|
10
|
+
Modal Open
|
|
11
|
+
<button
|
|
12
|
+
onClick={() => {
|
|
13
|
+
onToast('Thank you for your feedback!', 'success');
|
|
14
|
+
onClose();
|
|
15
|
+
}}
|
|
16
|
+
>
|
|
17
|
+
Submit Feedback
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
) : null;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
jest.mock('../components/Icon', () => ({ name }) => (
|
|
24
|
+
<img src={name} alt="icon" />
|
|
25
|
+
));
|
|
26
|
+
|
|
27
|
+
jest.mock('../components/markdown', () => ({
|
|
28
|
+
SVGIcon: ({ name }) => <img src={name} alt="icon" />,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('../../icons/thumbs-up.svg', () => 'thumbs-up.svg');
|
|
32
|
+
jest.mock('../../icons/thumbs-down.svg', () => 'thumbs-down.svg');
|
|
33
|
+
|
|
34
|
+
describe('ChatMessageFeedback', () => {
|
|
35
|
+
const defaultProps = {
|
|
36
|
+
message: {
|
|
37
|
+
messageId: 1,
|
|
38
|
+
message: 'Test message',
|
|
39
|
+
type: 'assistant',
|
|
40
|
+
},
|
|
41
|
+
feedbackReasons: ['Reason 1', 'Reason 2'],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
it('renders Like and Dislike buttons', () => {
|
|
45
|
+
render(<ChatMessageFeedback {...defaultProps} />);
|
|
46
|
+
expect(screen.getByLabelText('Like')).toBeInTheDocument();
|
|
47
|
+
expect(screen.getByLabelText('Dislike')).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('opens modal when Like is clicked', () => {
|
|
51
|
+
render(<ChatMessageFeedback {...defaultProps} />);
|
|
52
|
+
fireEvent.click(screen.getByLabelText('Like'));
|
|
53
|
+
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('opens modal when Dislike is clicked', () => {
|
|
57
|
+
render(<ChatMessageFeedback {...defaultProps} />);
|
|
58
|
+
fireEvent.click(screen.getByLabelText('Dislike'));
|
|
59
|
+
expect(screen.getByTestId('feedback-modal')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('shows toast after submitting feedback in modal', () => {
|
|
63
|
+
render(<ChatMessageFeedback {...defaultProps} />);
|
|
64
|
+
fireEvent.click(screen.getByLabelText('Like'));
|
|
65
|
+
|
|
66
|
+
const submitButton = screen.getByText('Submit Feedback');
|
|
67
|
+
fireEvent.click(submitButton);
|
|
68
|
+
|
|
69
|
+
expect(
|
|
70
|
+
screen.getByText('Thank you for your feedback!'),
|
|
71
|
+
).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { Citation } from '../components/markdown/Citation';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
describe('Citation', () => {
|
|
12
|
+
it('should render the component with link', () => {
|
|
13
|
+
const store = mockStore({
|
|
14
|
+
userSession: { token: '1234' },
|
|
15
|
+
intl: {
|
|
16
|
+
locale: 'en',
|
|
17
|
+
messages: {},
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const component = renderer.create(
|
|
22
|
+
<Provider store={store}>
|
|
23
|
+
<MemoryRouter>
|
|
24
|
+
<Citation
|
|
25
|
+
message={{
|
|
26
|
+
messageId: 6428,
|
|
27
|
+
message: 'Donec quam felis ultricies nec',
|
|
28
|
+
type: 'assistant',
|
|
29
|
+
query: 'Pellentesque libero tortor tincidunt et?',
|
|
30
|
+
documents: [
|
|
31
|
+
{
|
|
32
|
+
document_id: 'https://www.example.com',
|
|
33
|
+
semantic_identifier: 'Nam ipsum risus rutrum vitae',
|
|
34
|
+
link: 'https://www.example.com',
|
|
35
|
+
blurb:
|
|
36
|
+
'Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede. ',
|
|
37
|
+
source_type: 'web',
|
|
38
|
+
match_highlights: ['', 'Praesent ac sem eget est', ''],
|
|
39
|
+
updated_at: null,
|
|
40
|
+
db_doc_id: 99186,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
citations: {
|
|
44
|
+
1: 99186,
|
|
45
|
+
},
|
|
46
|
+
parentMessageId: 6427,
|
|
47
|
+
alternateAssistantID: null,
|
|
48
|
+
}}
|
|
49
|
+
value={['[1]']}
|
|
50
|
+
link="https://www.example.com"
|
|
51
|
+
/>
|
|
52
|
+
</MemoryRouter>
|
|
53
|
+
</Provider>,
|
|
54
|
+
);
|
|
55
|
+
const json = component.toJSON();
|
|
56
|
+
expect(json).toMatchSnapshot();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Citation', () => {
|
|
61
|
+
it('should render the component without link', () => {
|
|
62
|
+
const store = mockStore({
|
|
63
|
+
userSession: { token: '1234' },
|
|
64
|
+
intl: {
|
|
65
|
+
locale: 'en',
|
|
66
|
+
messages: {},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const component = renderer.create(
|
|
71
|
+
<Provider store={store}>
|
|
72
|
+
<MemoryRouter>
|
|
73
|
+
<Citation
|
|
74
|
+
message={{
|
|
75
|
+
messageId: 6428,
|
|
76
|
+
message: 'Donec quam felis ultricies nec',
|
|
77
|
+
type: 'assistant',
|
|
78
|
+
query: 'Pellentesque libero tortor tincidunt et?',
|
|
79
|
+
documents: [
|
|
80
|
+
{
|
|
81
|
+
document_id: 'https://www.example.com',
|
|
82
|
+
semantic_identifier: 'Nam ipsum risus rutrum vitae',
|
|
83
|
+
link: 'https://www.example.com',
|
|
84
|
+
blurb:
|
|
85
|
+
'Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede. ',
|
|
86
|
+
source_type: 'web',
|
|
87
|
+
match_highlights: ['', 'Praesent ac sem eget est', ''],
|
|
88
|
+
updated_at: null,
|
|
89
|
+
db_doc_id: 99186,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
citations: {
|
|
93
|
+
1: 99186,
|
|
94
|
+
},
|
|
95
|
+
parentMessageId: 6427,
|
|
96
|
+
alternateAssistantID: null,
|
|
97
|
+
}}
|
|
98
|
+
value={['[1]']}
|
|
99
|
+
link=""
|
|
100
|
+
/>
|
|
101
|
+
</MemoryRouter>
|
|
102
|
+
</Provider>,
|
|
103
|
+
);
|
|
104
|
+
const json = component.toJSON();
|
|
105
|
+
expect(json).toMatchSnapshot();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
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 EmptyState from '../components/EmptyState';
|
|
8
|
+
|
|
9
|
+
const mockStore = configureStore();
|
|
10
|
+
|
|
11
|
+
describe('EmptyState', () => {
|
|
12
|
+
let store;
|
|
13
|
+
let onChoiceMock;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
store = mockStore({
|
|
17
|
+
userSession: { token: '1234' },
|
|
18
|
+
intl: { locale: 'en', messages: {} },
|
|
19
|
+
});
|
|
20
|
+
onChoiceMock = jest.fn();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function renderComponent(props) {
|
|
24
|
+
return renderer.create(
|
|
25
|
+
<Provider store={store}>
|
|
26
|
+
<MemoryRouter>
|
|
27
|
+
<EmptyState {...props} />
|
|
28
|
+
</MemoryRouter>
|
|
29
|
+
</Provider>,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
it('renders with showAssistantPrompts and persona starter messages', () => {
|
|
34
|
+
const props = {
|
|
35
|
+
onChoice: onChoiceMock,
|
|
36
|
+
showAssistantPrompts: true,
|
|
37
|
+
enableStarterPrompts: false,
|
|
38
|
+
starterPromptsHeading: 'Starter Prompts',
|
|
39
|
+
persona: {
|
|
40
|
+
starter_messages: [
|
|
41
|
+
{
|
|
42
|
+
name: 'Starter 1',
|
|
43
|
+
description: 'Desc 1',
|
|
44
|
+
message: 'Message 1',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const component = renderComponent(props);
|
|
51
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('renders with enableStarterPrompts and starterPrompts', () => {
|
|
55
|
+
const props = {
|
|
56
|
+
onChoice: onChoiceMock,
|
|
57
|
+
showAssistantPrompts: false,
|
|
58
|
+
enableStarterPrompts: true,
|
|
59
|
+
starterPromptsHeading: 'Starter Prompts',
|
|
60
|
+
starterPrompts: [
|
|
61
|
+
{
|
|
62
|
+
name: 'Prompt 1',
|
|
63
|
+
description: 'Prompt Desc 1',
|
|
64
|
+
message: 'Prompt Message 1',
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const component = renderComponent(props);
|
|
70
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('renders nothing when no starter messages and no starter prompts', () => {
|
|
74
|
+
const props = {
|
|
75
|
+
onChoice: onChoiceMock,
|
|
76
|
+
showAssistantPrompts: true,
|
|
77
|
+
enableStarterPrompts: false,
|
|
78
|
+
persona: {
|
|
79
|
+
starter_messages: [],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const component = renderComponent(props);
|
|
84
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('renders without starterPromptsHeading', () => {
|
|
88
|
+
const props = {
|
|
89
|
+
onChoice: onChoiceMock,
|
|
90
|
+
showAssistantPrompts: true,
|
|
91
|
+
enableStarterPrompts: true,
|
|
92
|
+
starterPrompts: [
|
|
93
|
+
{
|
|
94
|
+
name: 'Prompt X',
|
|
95
|
+
description: 'Desc X',
|
|
96
|
+
message: 'Message X',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const component = renderComponent(props);
|
|
102
|
+
expect(component.toJSON()).toMatchSnapshot();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('calls onChoice when starter message button is clicked', () => {
|
|
106
|
+
const props = {
|
|
107
|
+
'@type': 'eeaChatbot',
|
|
108
|
+
assistant: '17',
|
|
109
|
+
chatTitle: 'Online public chat',
|
|
110
|
+
height: '500px',
|
|
111
|
+
onChoice: onChoiceMock,
|
|
112
|
+
showAssistantDescription: true,
|
|
113
|
+
showAssistantPrompts: true,
|
|
114
|
+
showAssistantTitle: true,
|
|
115
|
+
enableStarterPrompts: false,
|
|
116
|
+
persona: {
|
|
117
|
+
name: 'In enim justo rhoncus ut',
|
|
118
|
+
description: 'Nullam dictum felis eu pede',
|
|
119
|
+
starter_messages: [
|
|
120
|
+
{
|
|
121
|
+
description: 'Vestibulum purus quam scelerisque ut ',
|
|
122
|
+
message: 'Nam at tortor in tellus',
|
|
123
|
+
name: 'Curabitur at lacus ac velit',
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const component = renderComponent(props);
|
|
130
|
+
|
|
131
|
+
const button = component.root.findByProps({ className: 'starter-message' });
|
|
132
|
+
|
|
133
|
+
button.props.onClick();
|
|
134
|
+
|
|
135
|
+
expect(onChoiceMock).toHaveBeenCalledWith('Nam at tortor in tellus');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { render, screen, fireEvent, act } from '@testing-library/react';
|
|
2
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
3
|
+
import FeedbackModal from '../components/FeedbackModal';
|
|
4
|
+
import * as lib from '../utils';
|
|
5
|
+
|
|
6
|
+
jest.mock('../utils');
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
jest.clearAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('FeedbackModal', () => {
|
|
13
|
+
const baseProps = {
|
|
14
|
+
modalOpen: true,
|
|
15
|
+
onClose: jest.fn(),
|
|
16
|
+
setToast: jest.fn(),
|
|
17
|
+
onToast: jest.fn(),
|
|
18
|
+
setIsToastActive: jest.fn(),
|
|
19
|
+
message: { messageId: '1234' },
|
|
20
|
+
feedbackReasons: ['Reason 1', 'Reason 2'],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
it('submits positive feedback successfully', async () => {
|
|
24
|
+
lib.createChatMessageFeedback.mockResolvedValue({});
|
|
25
|
+
|
|
26
|
+
render(<FeedbackModal {...baseProps} isPositive={true} />);
|
|
27
|
+
|
|
28
|
+
fireEvent.change(
|
|
29
|
+
screen.getByPlaceholderText(/What did you like about this response/i),
|
|
30
|
+
{ target: { value: 'Great response!' } },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
await act(async () => {
|
|
34
|
+
fireEvent.click(screen.getByText('Submit Feedback'));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
expect(lib.createChatMessageFeedback).toHaveBeenCalledWith({
|
|
38
|
+
chat_message_id: '1234',
|
|
39
|
+
feedback_text: 'Great response!',
|
|
40
|
+
is_positive: true,
|
|
41
|
+
predefined_feedback: '',
|
|
42
|
+
});
|
|
43
|
+
expect(baseProps.onToast).toHaveBeenCalledWith(
|
|
44
|
+
'Thanks for your feedback!',
|
|
45
|
+
'success',
|
|
46
|
+
);
|
|
47
|
+
expect(baseProps.setIsToastActive).toHaveBeenCalledWith(true);
|
|
48
|
+
expect(baseProps.onClose).toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('submits negative feedback with selected reason', async () => {
|
|
52
|
+
lib.createChatMessageFeedback.mockResolvedValue({});
|
|
53
|
+
|
|
54
|
+
render(<FeedbackModal {...baseProps} isPositive={false} />);
|
|
55
|
+
|
|
56
|
+
const reasonBtn = screen.getByRole('button', { name: 'Reason 1' });
|
|
57
|
+
fireEvent.click(reasonBtn);
|
|
58
|
+
expect(reasonBtn.classList.contains('primary')).toBe(true);
|
|
59
|
+
|
|
60
|
+
fireEvent.change(screen.getByPlaceholderText(/What could be improved/i), {
|
|
61
|
+
target: { value: 'Needs more details' },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await act(async () => {
|
|
65
|
+
fireEvent.click(screen.getByText('Submit Feedback'));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(lib.createChatMessageFeedback).toHaveBeenCalledWith({
|
|
69
|
+
chat_message_id: '1234',
|
|
70
|
+
feedback_text: 'Needs more details',
|
|
71
|
+
is_positive: false,
|
|
72
|
+
predefined_feedback: 'Reason 1',
|
|
73
|
+
});
|
|
74
|
+
expect(baseProps.onToast).toHaveBeenCalledWith(
|
|
75
|
+
'Thanks for your feedback!',
|
|
76
|
+
'success',
|
|
77
|
+
);
|
|
78
|
+
expect(baseProps.onClose).toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('handles submission failure', async () => {
|
|
82
|
+
lib.createChatMessageFeedback.mockRejectedValue(new Error('fail'));
|
|
83
|
+
|
|
84
|
+
render(<FeedbackModal {...baseProps} isPositive={true} />);
|
|
85
|
+
|
|
86
|
+
await act(async () => {
|
|
87
|
+
fireEvent.click(screen.getByText('Submit Feedback'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(baseProps.onToast).toHaveBeenCalledWith(
|
|
91
|
+
'Failed to submit feedback.',
|
|
92
|
+
'error',
|
|
93
|
+
);
|
|
94
|
+
expect(baseProps.setIsToastActive).toHaveBeenCalledWith(true);
|
|
95
|
+
expect(baseProps.onClose).toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('resets and closes modal on Cancel click', () => {
|
|
99
|
+
render(<FeedbackModal {...baseProps} isPositive={true} />);
|
|
100
|
+
|
|
101
|
+
const textarea = screen.getByPlaceholderText(
|
|
102
|
+
/What did you like about this response/i,
|
|
103
|
+
);
|
|
104
|
+
fireEvent.change(textarea, { target: { value: 'Some feedback' } });
|
|
105
|
+
expect(textarea.value).toBe('Some feedback');
|
|
106
|
+
|
|
107
|
+
fireEvent.click(screen.getByText('Cancel'));
|
|
108
|
+
expect(baseProps.onClose).toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('toggles reason button selected state on click', () => {
|
|
112
|
+
render(<FeedbackModal {...baseProps} isPositive={false} />);
|
|
113
|
+
|
|
114
|
+
const reason1 = screen.getByRole('button', { name: 'Reason 1' });
|
|
115
|
+
const reason2 = screen.getByRole('button', { name: 'Reason 2' });
|
|
116
|
+
|
|
117
|
+
expect(reason1.classList.contains('inverted')).toBe(true);
|
|
118
|
+
expect(reason2.classList.contains('inverted')).toBe(true);
|
|
119
|
+
|
|
120
|
+
fireEvent.click(reason1);
|
|
121
|
+
expect(reason1.classList.contains('inverted')).toBe(false);
|
|
122
|
+
expect(reason2.classList.contains('inverted')).toBe(true);
|
|
123
|
+
|
|
124
|
+
fireEvent.click(reason2);
|
|
125
|
+
expect(reason1.classList.contains('inverted')).toBe(true);
|
|
126
|
+
expect(reason2.classList.contains('inverted')).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('updates textarea value on user input', () => {
|
|
130
|
+
render(<FeedbackModal {...baseProps} isPositive={true} />);
|
|
131
|
+
|
|
132
|
+
const textarea = screen.getByPlaceholderText(
|
|
133
|
+
/What did you like about this response/i,
|
|
134
|
+
);
|
|
135
|
+
fireEvent.change(textarea, { target: { value: 'Awesome!' } });
|
|
136
|
+
expect(textarea.value).toBe('Awesome!');
|
|
137
|
+
});
|
|
138
|
+
});
|