@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
package/razzle.extend.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const makeLoaderFinder = require('razzle-dev-utils/makeLoaderFinder');
|
|
3
|
+
|
|
4
|
+
const modify = (config, { target, dev }, webpack) => {
|
|
5
|
+
const markedPath = path.dirname(require.resolve('marked'));
|
|
6
|
+
// const nodeFetch = path.dirname(require.resolve('node-fetch'));
|
|
7
|
+
|
|
8
|
+
const babelLoaderFinder = makeLoaderFinder('babel-loader');
|
|
9
|
+
const babelLoader = config.module.rules.find(babelLoaderFinder);
|
|
10
|
+
|
|
11
|
+
// config.module.rules.push({
|
|
12
|
+
// test: /node_modules\/vfile\//, // \/lib\/index\.js
|
|
13
|
+
// use: [
|
|
14
|
+
// {
|
|
15
|
+
// loader: 'imports-loader',
|
|
16
|
+
// options: {
|
|
17
|
+
// type: 'commonjs',
|
|
18
|
+
// // imports: ['single process/browser process'],
|
|
19
|
+
// },
|
|
20
|
+
// },
|
|
21
|
+
// ],
|
|
22
|
+
// });
|
|
23
|
+
|
|
24
|
+
const { include } = babelLoader;
|
|
25
|
+
|
|
26
|
+
include.push(markedPath);
|
|
27
|
+
// include.push(nodeFetch);
|
|
28
|
+
|
|
29
|
+
babelLoader.use[0].options.plugins = [
|
|
30
|
+
...(babelLoader.use[0].options.plugins || []),
|
|
31
|
+
'@babel/plugin-proposal-private-methods',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
return config;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
plugins: (plugs) => plugs,
|
|
39
|
+
modify,
|
|
40
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { compose } from 'redux';
|
|
3
|
+
import superagent from 'superagent';
|
|
4
|
+
import { BlockDataForm, SidebarPortal } from '@plone/volto/components';
|
|
5
|
+
|
|
6
|
+
import ChatBlockView from './ChatBlockView';
|
|
7
|
+
import { ChatBlockSchema } from './schema';
|
|
8
|
+
import withOnyxData from './hocs/withOnyxData';
|
|
9
|
+
|
|
10
|
+
const ChatBlockEdit = (props) => {
|
|
11
|
+
const { onChangeBlock, block, assistants, data } = props;
|
|
12
|
+
|
|
13
|
+
const schema = React.useMemo(
|
|
14
|
+
() => ChatBlockSchema({ assistants, data }),
|
|
15
|
+
[assistants, data],
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div>
|
|
20
|
+
<ChatBlockView {...props} isEditMode />
|
|
21
|
+
<SidebarPortal selected={props.selected}>
|
|
22
|
+
<BlockDataForm
|
|
23
|
+
schema={schema}
|
|
24
|
+
title={schema.title}
|
|
25
|
+
block={block}
|
|
26
|
+
onChangeBlock={onChangeBlock}
|
|
27
|
+
onChangeField={(id, value) => {
|
|
28
|
+
onChangeBlock(props.block, {
|
|
29
|
+
...props.data,
|
|
30
|
+
[id]: value,
|
|
31
|
+
});
|
|
32
|
+
}}
|
|
33
|
+
formData={props.data}
|
|
34
|
+
/>
|
|
35
|
+
</SidebarPortal>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default compose(
|
|
41
|
+
withOnyxData(() => [
|
|
42
|
+
'assistants',
|
|
43
|
+
superagent.get('/_da/persona?include_deleted=false').type('json'),
|
|
44
|
+
]),
|
|
45
|
+
// withOnyxData(() => ['tool', superagent.get('/_da/tool').type('json')]), // May be needed in the future
|
|
46
|
+
)(ChatBlockEdit);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import superagent from 'superagent';
|
|
2
|
+
import withOnyxData from './hocs/withOnyxData';
|
|
3
|
+
import { ChatWindow } from './chat';
|
|
4
|
+
|
|
5
|
+
function ChatBlockView(props) {
|
|
6
|
+
const { assistantData, data, isEditMode } = props;
|
|
7
|
+
|
|
8
|
+
return assistantData ? (
|
|
9
|
+
<ChatWindow persona={assistantData} isEditMode={isEditMode} {...data} />
|
|
10
|
+
) : (
|
|
11
|
+
<div>Chatbot</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default withOnyxData((props) => [
|
|
16
|
+
'assistantData',
|
|
17
|
+
typeof props.data?.assistant !== 'undefined'
|
|
18
|
+
? superagent.get(`/_da/persona/${props.data.assistant}`).type('json')
|
|
19
|
+
: null,
|
|
20
|
+
props.data?.assistant,
|
|
21
|
+
])(ChatBlockView);
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import type { ChatMessageProps } from '../types/interfaces';
|
|
2
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
3
|
+
import cx from 'classnames';
|
|
4
|
+
import visit from 'unist-util-visit';
|
|
5
|
+
import loadable from '@loadable/component';
|
|
6
|
+
import {
|
|
7
|
+
Tab,
|
|
8
|
+
Sidebar,
|
|
9
|
+
Button,
|
|
10
|
+
Message as SemanticMessage,
|
|
11
|
+
} from 'semantic-ui-react';
|
|
12
|
+
import { serializeNodes } from '@plone/volto-slate/editor/render';
|
|
13
|
+
import {
|
|
14
|
+
useDeepCompareMemoize,
|
|
15
|
+
useQualityMarkers,
|
|
16
|
+
useScrollonStream,
|
|
17
|
+
} from '../hooks';
|
|
18
|
+
import { MultiToolRenderer, RendererComponent } from '../packets';
|
|
19
|
+
import { addCitations } from '../utils/citations';
|
|
20
|
+
import SVGIcon from '../components/Icon';
|
|
21
|
+
import BotIcon from '../../icons/bot.svg';
|
|
22
|
+
import ClearIcon from '../../icons/clear.svg';
|
|
23
|
+
|
|
24
|
+
// Lazy load heavy components
|
|
25
|
+
const SourceDetails: any = loadable(() => import('../components/Source'));
|
|
26
|
+
const UserActionsToolbar: any = loadable(
|
|
27
|
+
() => import('../components/UserActionsToolbar'),
|
|
28
|
+
);
|
|
29
|
+
const RelatedQuestions: any = loadable(
|
|
30
|
+
() => import('../components/RelatedQuestions'),
|
|
31
|
+
);
|
|
32
|
+
const HalloumiFeedback: any = loadable(
|
|
33
|
+
() => import('../components/HalloumiFeedback'),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function capitalize(str: string) {
|
|
37
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addQualityMarkersPlugin() {
|
|
41
|
+
return function (tree: any) {
|
|
42
|
+
visit(tree, 'element', function (node: any) {
|
|
43
|
+
node.children?.forEach((child: any, cidx: any) => {
|
|
44
|
+
if (child.type === 'raw' && child.value?.trim() === '<br>') {
|
|
45
|
+
const newNode = {
|
|
46
|
+
...child,
|
|
47
|
+
type: 'element',
|
|
48
|
+
tagName: 'br',
|
|
49
|
+
children: [],
|
|
50
|
+
value: '',
|
|
51
|
+
};
|
|
52
|
+
node.children[cidx] = newNode;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
visit(tree, 'text', function (node: any, idx: any, parent: any) {
|
|
57
|
+
if (node.value?.trim()) {
|
|
58
|
+
const newNode = {
|
|
59
|
+
type: 'element',
|
|
60
|
+
tagName: 'span',
|
|
61
|
+
children: [node],
|
|
62
|
+
};
|
|
63
|
+
parent.children[idx] = newNode;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function addHalloumiContext(doc: any, text: string) {
|
|
70
|
+
const updatedDate = doc.updated_at
|
|
71
|
+
? new Date(doc.updated_at).toLocaleString('en-GB', {
|
|
72
|
+
year: 'numeric',
|
|
73
|
+
month: 'long',
|
|
74
|
+
day: '2-digit',
|
|
75
|
+
hour: '2-digit',
|
|
76
|
+
minute: '2-digit',
|
|
77
|
+
})
|
|
78
|
+
: '';
|
|
79
|
+
|
|
80
|
+
const docIndex = doc.index ? `DOCUMENT ${doc.index}: ` : '';
|
|
81
|
+
const sources: any = { web: 'Website', file: 'File' };
|
|
82
|
+
|
|
83
|
+
const sourceType = doc.source_type
|
|
84
|
+
? sources[doc.source_type] || capitalize(doc.source_type)
|
|
85
|
+
: '';
|
|
86
|
+
|
|
87
|
+
const header = `${docIndex}${doc.semantic_identifier}${
|
|
88
|
+
sourceType ? `\nSource: ${sourceType}` : ''
|
|
89
|
+
}${updatedDate ? `\nUpdated: ${updatedDate}` : ''}`;
|
|
90
|
+
|
|
91
|
+
return `${header}\n${text}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mapToolDocumentsToText(message: any) {
|
|
95
|
+
if (!message?.toolCall?.tool_result) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const toolResult = message.toolCall.tool_result;
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(toolResult)) {
|
|
102
|
+
return toolResult.reduce((acc: Record<string, string>, doc: any) => {
|
|
103
|
+
if (doc.document_id && doc.content) {
|
|
104
|
+
acc[doc.document_id] = doc.content;
|
|
105
|
+
}
|
|
106
|
+
return acc;
|
|
107
|
+
}, {});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getContextSources(
|
|
114
|
+
message: any,
|
|
115
|
+
sources: any,
|
|
116
|
+
qualityCheckContext: any,
|
|
117
|
+
) {
|
|
118
|
+
const documentIdToText = mapToolDocumentsToText(message);
|
|
119
|
+
|
|
120
|
+
return qualityCheckContext === 'citations'
|
|
121
|
+
? sources.map((doc: any) => ({
|
|
122
|
+
...doc,
|
|
123
|
+
id: doc.document_id,
|
|
124
|
+
text: documentIdToText[doc.document_id] || '',
|
|
125
|
+
halloumiContext: addHalloumiContext(
|
|
126
|
+
doc,
|
|
127
|
+
documentIdToText[doc.document_id] || '',
|
|
128
|
+
),
|
|
129
|
+
}))
|
|
130
|
+
: (message.toolCalls || []).reduce(
|
|
131
|
+
(acc: any, cur: any) => [
|
|
132
|
+
...acc,
|
|
133
|
+
...(cur.tool_result || []).map((doc: any) => ({
|
|
134
|
+
...doc,
|
|
135
|
+
id: doc.document_id,
|
|
136
|
+
text: doc.content,
|
|
137
|
+
halloumiContext: addHalloumiContext(doc, doc.content),
|
|
138
|
+
})),
|
|
139
|
+
], // TODO: make sure we don't add multiple times the same doc
|
|
140
|
+
// TODO: this doesn't have the index for source
|
|
141
|
+
[],
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getScoreDetails(claims: any, qualityCheckStages: any) {
|
|
146
|
+
const score = (
|
|
147
|
+
(claims.length > 0
|
|
148
|
+
? claims.reduce((acc: any, { score }: any) => acc + score, 0) /
|
|
149
|
+
claims.length
|
|
150
|
+
: 1) * 100
|
|
151
|
+
).toFixed(0);
|
|
152
|
+
|
|
153
|
+
const scoreStage = qualityCheckStages?.find(
|
|
154
|
+
({ start, end }: any) => start <= score && score <= end,
|
|
155
|
+
);
|
|
156
|
+
const isFirstScoreStage =
|
|
157
|
+
qualityCheckStages?.reduce(
|
|
158
|
+
(acc: any, { start, end }: any, curIx: any) =>
|
|
159
|
+
start <= score && score <= end ? curIx : acc,
|
|
160
|
+
-1,
|
|
161
|
+
) ?? -1;
|
|
162
|
+
const scoreColor = scoreStage?.color || 'black';
|
|
163
|
+
return { score, scoreStage, isFirstScoreStage, scoreColor };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function AIMessage({
|
|
167
|
+
message,
|
|
168
|
+
isLoading,
|
|
169
|
+
libs,
|
|
170
|
+
onChoice,
|
|
171
|
+
onFetchRelatedQuestions,
|
|
172
|
+
enableFeedback,
|
|
173
|
+
scrollToInput,
|
|
174
|
+
feedbackReasons,
|
|
175
|
+
qualityCheck,
|
|
176
|
+
qualityCheckStages,
|
|
177
|
+
qualityCheckContext,
|
|
178
|
+
qualityCheckEnabled,
|
|
179
|
+
noSupportDocumentsMessage,
|
|
180
|
+
totalFailMessage,
|
|
181
|
+
isFetchingRelatedQuestions = false,
|
|
182
|
+
enableShowTotalFailMessage,
|
|
183
|
+
enableMatomoTracking,
|
|
184
|
+
persona,
|
|
185
|
+
maxContextSegments,
|
|
186
|
+
isLastMessage,
|
|
187
|
+
className = '',
|
|
188
|
+
chatWindowEndRef,
|
|
189
|
+
showTools,
|
|
190
|
+
}: ChatMessageProps) {
|
|
191
|
+
const [allToolsDisplayed, setAllToolsDisplayed] = useState(false);
|
|
192
|
+
const [messageDisplayed, setMessageDisplayed] = useState(false);
|
|
193
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
194
|
+
const [showSourcesSidebar, setShowSourcesSidebar] = useState(false);
|
|
195
|
+
// Halloumi
|
|
196
|
+
const [forceHalloumi, setForceHallomi] = useState(qualityCheck === 'enabled');
|
|
197
|
+
const [verificationTriggered, setVerificationTriggered] = useState(false);
|
|
198
|
+
const [isMessageVerified, setIsMessageVerified] = useState(false);
|
|
199
|
+
|
|
200
|
+
useScrollonStream({
|
|
201
|
+
bottomRef: chatWindowEndRef,
|
|
202
|
+
isStreaming: isLoading || !messageDisplayed || isFetchingRelatedQuestions,
|
|
203
|
+
enabled: scrollToInput,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const {
|
|
207
|
+
groupedPackets = [],
|
|
208
|
+
toolPackets = [],
|
|
209
|
+
displayPackets = [],
|
|
210
|
+
citations = {},
|
|
211
|
+
documents = [],
|
|
212
|
+
relatedQuestions,
|
|
213
|
+
isComplete = false,
|
|
214
|
+
error,
|
|
215
|
+
} = message;
|
|
216
|
+
|
|
217
|
+
// Separate tool groups from display groups
|
|
218
|
+
const toolGroups = useMemo(() => {
|
|
219
|
+
return groupedPackets.filter((group) => toolPackets.includes(group.ind));
|
|
220
|
+
}, [groupedPackets, toolPackets]);
|
|
221
|
+
|
|
222
|
+
const displayGroups = useMemo(() => {
|
|
223
|
+
return groupedPackets.filter((group) => displayPackets.includes(group.ind));
|
|
224
|
+
}, [groupedPackets, displayPackets]);
|
|
225
|
+
|
|
226
|
+
const onAllToolsDisplayed = useMemo(() => {
|
|
227
|
+
return () => {
|
|
228
|
+
setAllToolsDisplayed(true);
|
|
229
|
+
};
|
|
230
|
+
}, []);
|
|
231
|
+
|
|
232
|
+
// Build sources from citations
|
|
233
|
+
const inverseMap = useMemo(
|
|
234
|
+
() =>
|
|
235
|
+
Object.entries(citations).reduce(
|
|
236
|
+
(acc, [k, v]) => {
|
|
237
|
+
return { ...acc, [v]: k };
|
|
238
|
+
},
|
|
239
|
+
{} as Record<string, string>,
|
|
240
|
+
),
|
|
241
|
+
[citations],
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const sources = useMemo(
|
|
245
|
+
() =>
|
|
246
|
+
Object.values(citations).map((doc_id) => {
|
|
247
|
+
const doc = documents?.find((doc: any) => doc.document_id === doc_id);
|
|
248
|
+
return {
|
|
249
|
+
...(doc || {}),
|
|
250
|
+
index: inverseMap[doc_id],
|
|
251
|
+
};
|
|
252
|
+
}),
|
|
253
|
+
[citations, documents, inverseMap],
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const showSources = messageDisplayed && sources.length > 0;
|
|
257
|
+
|
|
258
|
+
const contextSources = getContextSources(
|
|
259
|
+
message,
|
|
260
|
+
sources,
|
|
261
|
+
qualityCheckContext,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const stableContextSources = useDeepCompareMemoize(contextSources);
|
|
265
|
+
|
|
266
|
+
const doQualityControl =
|
|
267
|
+
messageDisplayed &&
|
|
268
|
+
qualityCheck &&
|
|
269
|
+
qualityCheck !== 'disabled' &&
|
|
270
|
+
forceHalloumi &&
|
|
271
|
+
showSources &&
|
|
272
|
+
message.messageId &&
|
|
273
|
+
message.messageId > -1 &&
|
|
274
|
+
(qualityCheck === 'enabled' ||
|
|
275
|
+
qualityCheckEnabled ||
|
|
276
|
+
verificationTriggered);
|
|
277
|
+
|
|
278
|
+
const { markers, isLoadingHalloumi, retryHalloumi }: any = useQualityMarkers(
|
|
279
|
+
doQualityControl,
|
|
280
|
+
addCitations(message.message, message),
|
|
281
|
+
stableContextSources,
|
|
282
|
+
maxContextSegments,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const claims = markers?.claims || [];
|
|
286
|
+
const { score, scoreStage, scoreColor, isFirstScoreStage } = getScoreDetails(
|
|
287
|
+
claims,
|
|
288
|
+
qualityCheckStages,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const isFetching = isLoadingHalloumi || isLoading;
|
|
292
|
+
const halloumiMessage =
|
|
293
|
+
isMessageVerified || doQualityControl ? scoreStage?.label : '';
|
|
294
|
+
|
|
295
|
+
const showVerifyClaimsButton =
|
|
296
|
+
messageDisplayed &&
|
|
297
|
+
sources.length > 0 &&
|
|
298
|
+
!isFetching &&
|
|
299
|
+
!markers &&
|
|
300
|
+
(qualityCheck === 'ondemand' ||
|
|
301
|
+
(qualityCheck === 'ondemand_toggle' && !qualityCheckEnabled));
|
|
302
|
+
|
|
303
|
+
const showTotalFailMessage =
|
|
304
|
+
messageDisplayed &&
|
|
305
|
+
sources.length === 0 &&
|
|
306
|
+
!isFetching &&
|
|
307
|
+
enableShowTotalFailMessage;
|
|
308
|
+
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
if (isFetchingRelatedQuestions || typeof relatedQuestions !== 'undefined') {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (messageDisplayed && isComplete && onFetchRelatedQuestions) {
|
|
314
|
+
onFetchRelatedQuestions();
|
|
315
|
+
}
|
|
316
|
+
}, [
|
|
317
|
+
messageDisplayed,
|
|
318
|
+
relatedQuestions,
|
|
319
|
+
isComplete,
|
|
320
|
+
onFetchRelatedQuestions,
|
|
321
|
+
isFetchingRelatedQuestions,
|
|
322
|
+
]);
|
|
323
|
+
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
if (qualityCheck === 'ondemand_toggle' && qualityCheckEnabled) {
|
|
326
|
+
setForceHallomi(true);
|
|
327
|
+
} else if (qualityCheck !== 'enabled') {
|
|
328
|
+
setForceHallomi(false);
|
|
329
|
+
}
|
|
330
|
+
}, [qualityCheck, qualityCheckEnabled]);
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (markers?.claims?.length > 0) {
|
|
334
|
+
setIsMessageVerified(true);
|
|
335
|
+
}
|
|
336
|
+
}, [markers]);
|
|
337
|
+
|
|
338
|
+
// Answer tab content
|
|
339
|
+
const answerTab = (
|
|
340
|
+
<div className="answer-tab">
|
|
341
|
+
{/* Show first 3 sources inline */}
|
|
342
|
+
{showSources && (
|
|
343
|
+
<div className="sources">
|
|
344
|
+
{sources.slice(0, 3).map((source: any, i: number) => (
|
|
345
|
+
<SourceDetails source={source} key={i} index={source.index} />
|
|
346
|
+
))}
|
|
347
|
+
|
|
348
|
+
{sources.length > 3 && (
|
|
349
|
+
<Button
|
|
350
|
+
className="source show-all-sources-btn"
|
|
351
|
+
onClick={() => setShowSourcesSidebar(true)}
|
|
352
|
+
>
|
|
353
|
+
<div className="source-header">
|
|
354
|
+
<div>
|
|
355
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
356
|
+
<span key={i} className="chat-citation"></span>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
<div className="source-title">See all sources</div>
|
|
360
|
+
</div>
|
|
361
|
+
</Button>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{/* Main message content */}
|
|
367
|
+
<div className="message-content">
|
|
368
|
+
{/* Render tools if any */}
|
|
369
|
+
{toolGroups.length > 0 && (
|
|
370
|
+
<MultiToolRenderer
|
|
371
|
+
toolGroups={toolGroups}
|
|
372
|
+
showTools={showTools}
|
|
373
|
+
onAllToolsDisplayed={onAllToolsDisplayed}
|
|
374
|
+
message={message}
|
|
375
|
+
libs={libs}
|
|
376
|
+
/>
|
|
377
|
+
)}
|
|
378
|
+
|
|
379
|
+
{/* Display error message if present */}
|
|
380
|
+
{!!error && (
|
|
381
|
+
<div className="message-error">
|
|
382
|
+
<SemanticMessage color="red" className="error-message">
|
|
383
|
+
<div className="error-title">Error</div>
|
|
384
|
+
<div className="error-content">{error}</div>
|
|
385
|
+
</SemanticMessage>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{/* Display normal content if no error or if we have content to display alongside the error */}
|
|
390
|
+
{(allToolsDisplayed ||
|
|
391
|
+
toolGroups.length === 0 ||
|
|
392
|
+
message.isFinalMessageComing) &&
|
|
393
|
+
!error &&
|
|
394
|
+
displayGroups.map((group) => (
|
|
395
|
+
<div key={group.ind} className="message-display-group">
|
|
396
|
+
<RendererComponent
|
|
397
|
+
packets={group.packets}
|
|
398
|
+
onComplete={() => setMessageDisplayed(true)}
|
|
399
|
+
animate={!messageDisplayed}
|
|
400
|
+
stopPacketSeen={isComplete}
|
|
401
|
+
message={message}
|
|
402
|
+
libs={libs}
|
|
403
|
+
markers={markers}
|
|
404
|
+
stableContextSources={stableContextSources}
|
|
405
|
+
addQualityMarkersPlugin={addQualityMarkersPlugin}
|
|
406
|
+
>
|
|
407
|
+
{({ content }) => (
|
|
408
|
+
<div className="message-text-wrapper">{content}</div>
|
|
409
|
+
)}
|
|
410
|
+
</RendererComponent>
|
|
411
|
+
</div>
|
|
412
|
+
))}
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
{/* Total fail message */}
|
|
416
|
+
{showTotalFailMessage && (
|
|
417
|
+
<SemanticMessage color="red">
|
|
418
|
+
{serializeNodes(totalFailMessage)}
|
|
419
|
+
</SemanticMessage>
|
|
420
|
+
)}
|
|
421
|
+
|
|
422
|
+
{/* Halloumi/Quality feedback */}
|
|
423
|
+
{qualityCheck !== 'disabled' && !error && (
|
|
424
|
+
<HalloumiFeedback
|
|
425
|
+
sources={sources}
|
|
426
|
+
halloumiMessage={halloumiMessage}
|
|
427
|
+
isLoadingHalloumi={isLoadingHalloumi}
|
|
428
|
+
markers={markers}
|
|
429
|
+
score={score}
|
|
430
|
+
scoreColor={scoreColor}
|
|
431
|
+
onManualVerify={() => {
|
|
432
|
+
setForceHallomi(true);
|
|
433
|
+
setVerificationTriggered(true);
|
|
434
|
+
}}
|
|
435
|
+
showVerifyClaimsButton={showVerifyClaimsButton}
|
|
436
|
+
retryHalloumi={retryHalloumi}
|
|
437
|
+
/>
|
|
438
|
+
)}
|
|
439
|
+
|
|
440
|
+
{/* User actions toolbar (feedback, copy, etc) */}
|
|
441
|
+
{!isLoading && (
|
|
442
|
+
<UserActionsToolbar
|
|
443
|
+
message={message}
|
|
444
|
+
enableFeedback={enableFeedback}
|
|
445
|
+
feedbackReasons={feedbackReasons}
|
|
446
|
+
enableMatomoTracking={enableMatomoTracking}
|
|
447
|
+
persona={persona}
|
|
448
|
+
/>
|
|
449
|
+
)}
|
|
450
|
+
|
|
451
|
+
{isFirstScoreStage === -1 && serializeNodes(noSupportDocumentsMessage)}
|
|
452
|
+
|
|
453
|
+
{isFetchingRelatedQuestions && isLastMessage && !error && (
|
|
454
|
+
<SemanticMessage color="blue">
|
|
455
|
+
<div className="related-questions-loader">
|
|
456
|
+
Finding related questions...
|
|
457
|
+
</div>
|
|
458
|
+
</SemanticMessage>
|
|
459
|
+
)}
|
|
460
|
+
|
|
461
|
+
{/* Related questions */}
|
|
462
|
+
|
|
463
|
+
{!error && (
|
|
464
|
+
<RelatedQuestions
|
|
465
|
+
persona={persona}
|
|
466
|
+
message={message}
|
|
467
|
+
isLoading={isLoading}
|
|
468
|
+
onChoice={onChoice}
|
|
469
|
+
enableMatomoTracking={enableMatomoTracking}
|
|
470
|
+
/>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Tab panes - conditionally include Sources tab
|
|
476
|
+
const panes = [
|
|
477
|
+
{ menuItem: 'Answer', pane: <Tab.Pane key="answer">{answerTab}</Tab.Pane> },
|
|
478
|
+
...(showSources && !error
|
|
479
|
+
? [
|
|
480
|
+
{
|
|
481
|
+
menuItem: {
|
|
482
|
+
key: 'sources',
|
|
483
|
+
content: (
|
|
484
|
+
<span>
|
|
485
|
+
Sources{' '}
|
|
486
|
+
<span className="sources-count">({sources.length})</span>
|
|
487
|
+
</span>
|
|
488
|
+
),
|
|
489
|
+
},
|
|
490
|
+
pane: (
|
|
491
|
+
<Tab.Pane key="sources">
|
|
492
|
+
<div className="sources-listing">
|
|
493
|
+
<div className="sources">
|
|
494
|
+
{sources.map((source: any, i: number) => (
|
|
495
|
+
<SourceDetails
|
|
496
|
+
source={source}
|
|
497
|
+
key={i}
|
|
498
|
+
index={source.index}
|
|
499
|
+
/>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</Tab.Pane>
|
|
504
|
+
),
|
|
505
|
+
},
|
|
506
|
+
]
|
|
507
|
+
: []),
|
|
508
|
+
];
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<div className={`comment ${className}`}>
|
|
512
|
+
<div className="circle assistant">
|
|
513
|
+
<SVGIcon name={BotIcon} size={20} color="white" />
|
|
514
|
+
</div>
|
|
515
|
+
|
|
516
|
+
<div className="comment-content">
|
|
517
|
+
{/* Main content with tabs */}
|
|
518
|
+
<div className="comment-tabs">
|
|
519
|
+
<Tab
|
|
520
|
+
activeIndex={activeTab}
|
|
521
|
+
onTabChange={(_, data: any) => setActiveTab(data.activeIndex)}
|
|
522
|
+
menu={{
|
|
523
|
+
secondary: true,
|
|
524
|
+
pointing: true,
|
|
525
|
+
fluid: true,
|
|
526
|
+
className: cx({ 'without-sources': !showSources }),
|
|
527
|
+
}}
|
|
528
|
+
panes={panes}
|
|
529
|
+
renderActiveOnly={false}
|
|
530
|
+
/>
|
|
531
|
+
|
|
532
|
+
{/* Sources sidebar */}
|
|
533
|
+
{showSources && !error && (
|
|
534
|
+
<Sidebar
|
|
535
|
+
visible={showSourcesSidebar}
|
|
536
|
+
animation="overlay"
|
|
537
|
+
icon="labeled"
|
|
538
|
+
width="wide"
|
|
539
|
+
direction="right"
|
|
540
|
+
className="sources-sidebar"
|
|
541
|
+
onHide={() => setShowSourcesSidebar(false)}
|
|
542
|
+
>
|
|
543
|
+
<div className="sources-sidebar-heading">
|
|
544
|
+
<h4>Sources</h4>
|
|
545
|
+
<Button basic onClick={() => setShowSourcesSidebar(false)}>
|
|
546
|
+
<SVGIcon name={ClearIcon} size={24} />
|
|
547
|
+
</Button>
|
|
548
|
+
</div>
|
|
549
|
+
<div className="sources-listing">
|
|
550
|
+
<div className="sources">
|
|
551
|
+
{sources.map((source: any, i: number) => (
|
|
552
|
+
<SourceDetails
|
|
553
|
+
source={source}
|
|
554
|
+
key={i}
|
|
555
|
+
index={source.index}
|
|
556
|
+
/>
|
|
557
|
+
))}
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
</Sidebar>
|
|
561
|
+
)}
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Message as SemanticMessage } from 'semantic-ui-react';
|
|
2
|
+
import type { ChatMessageProps } from '../types/interfaces';
|
|
3
|
+
import { UserMessage, AIMessage } from '.';
|
|
4
|
+
|
|
5
|
+
export function ChatMessage(props: ChatMessageProps) {
|
|
6
|
+
const { message, libs, className = '' } = props;
|
|
7
|
+
|
|
8
|
+
if (message.type === 'user') {
|
|
9
|
+
return (
|
|
10
|
+
<UserMessage
|
|
11
|
+
message={message}
|
|
12
|
+
libs={libs}
|
|
13
|
+
className={className}
|
|
14
|
+
isLoading={props.isLoading}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (message.type === 'assistant') {
|
|
20
|
+
return <AIMessage {...props} />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (message.type === 'error') {
|
|
24
|
+
return (
|
|
25
|
+
<div className="message-error">
|
|
26
|
+
<SemanticMessage color="red" className="error-message">
|
|
27
|
+
<div className="error-title">Error</div>
|
|
28
|
+
<div className="error-content">{message.error}</div>
|
|
29
|
+
</SemanticMessage>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|