@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,126 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import { Button, Message, MessageContent } from 'semantic-ui-react';
|
|
4
|
+
import { serializeNodes } from '@plone/volto-slate/editor/render';
|
|
5
|
+
import Spinner from './Spinner';
|
|
6
|
+
import SVGIcon from './Icon';
|
|
7
|
+
import { getSupportedBgColor } from './markdown/colors';
|
|
8
|
+
|
|
9
|
+
import GlassesIcon from '../../icons/glasses.svg';
|
|
10
|
+
import RotateIcon from '../../icons/rotate.svg';
|
|
11
|
+
|
|
12
|
+
const VERIFY_CLAIM_MESSAGES = [
|
|
13
|
+
'Going through each claim and verify against the referenced documents...',
|
|
14
|
+
'Summarising claim verifications results...',
|
|
15
|
+
'Calculating scores...',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function visitTextNodes(node, visitor) {
|
|
19
|
+
if (Array.isArray(node)) {
|
|
20
|
+
node.forEach((child) => visitTextNodes(child, visitor));
|
|
21
|
+
} else if (node && typeof node === 'object') {
|
|
22
|
+
if (node.text !== undefined) {
|
|
23
|
+
visitor(node);
|
|
24
|
+
}
|
|
25
|
+
if (node.children) {
|
|
26
|
+
visitTextNodes(node.children, visitor);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function printSlate(value, score) {
|
|
32
|
+
if (typeof value === 'string') {
|
|
33
|
+
return value.replaceAll('{score}', score);
|
|
34
|
+
}
|
|
35
|
+
function visitor(node) {
|
|
36
|
+
if (node.text.indexOf('{score}') > -1) {
|
|
37
|
+
node.text = node.text.replaceAll('{score}', score);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
visitTextNodes(value, visitor);
|
|
42
|
+
return serializeNodes(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function VerifyClaims() {
|
|
46
|
+
const [message, setMessage] = React.useState(0);
|
|
47
|
+
|
|
48
|
+
React.useEffect(() => {
|
|
49
|
+
const timer = setTimeout(() => {
|
|
50
|
+
if (message < VERIFY_CLAIM_MESSAGES.length - 1) {
|
|
51
|
+
setMessage(message + 1);
|
|
52
|
+
}
|
|
53
|
+
}, 5000);
|
|
54
|
+
return () => clearTimeout(timer);
|
|
55
|
+
}, [message]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="verify-claims">
|
|
59
|
+
<Spinner />
|
|
60
|
+
{VERIFY_CLAIM_MESSAGES[message]}
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const HalloumiFeedback = ({
|
|
66
|
+
halloumiMessage,
|
|
67
|
+
isLoadingHalloumi,
|
|
68
|
+
markers,
|
|
69
|
+
score,
|
|
70
|
+
scoreColor,
|
|
71
|
+
onManualVerify,
|
|
72
|
+
showVerifyClaimsButton,
|
|
73
|
+
sources,
|
|
74
|
+
retryHalloumi,
|
|
75
|
+
}) => {
|
|
76
|
+
const noClaimsScore = markers?.claims[0]?.score === null;
|
|
77
|
+
const messageBySource =
|
|
78
|
+
'Please allow a few minutes for claim verification when many references are involved.';
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<>
|
|
82
|
+
{showVerifyClaimsButton && (
|
|
83
|
+
<div className="halloumi-feedback-button">
|
|
84
|
+
<Button onClick={onManualVerify} className="icon claims-btn">
|
|
85
|
+
<SVGIcon name={GlassesIcon} /> Fact-check AI answer
|
|
86
|
+
</Button>
|
|
87
|
+
<div>
|
|
88
|
+
<span>{messageBySource}</span>{' '}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{isLoadingHalloumi && sources.length > 0 && (
|
|
94
|
+
<Message color="blue">
|
|
95
|
+
<VerifyClaims />
|
|
96
|
+
</Message>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{noClaimsScore && (
|
|
100
|
+
<>
|
|
101
|
+
<Message color="red">{markers?.claims?.[0].rationale}</Message>
|
|
102
|
+
<Button onClick={retryHalloumi} className="icon">
|
|
103
|
+
<SVGIcon name={RotateIcon} /> Retry Fact-check AI answer
|
|
104
|
+
</Button>
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{!!halloumiMessage && !!markers && !noClaimsScore && (
|
|
109
|
+
<Message
|
|
110
|
+
color={scoreColor}
|
|
111
|
+
className={cx(
|
|
112
|
+
'claim-message',
|
|
113
|
+
getSupportedBgColor(score / 100, 'claim'),
|
|
114
|
+
)}
|
|
115
|
+
icon
|
|
116
|
+
>
|
|
117
|
+
<MessageContent>
|
|
118
|
+
{printSlate(halloumiMessage, `${score}%`)}
|
|
119
|
+
</MessageContent>
|
|
120
|
+
</Message>
|
|
121
|
+
)}
|
|
122
|
+
</>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default HalloumiFeedback;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
type IconProps = {
|
|
2
|
+
name: any;
|
|
3
|
+
size?: number;
|
|
4
|
+
color?: string;
|
|
5
|
+
className?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const Icon = ({
|
|
10
|
+
name,
|
|
11
|
+
size = 25,
|
|
12
|
+
color = 'currentColor',
|
|
13
|
+
className = '',
|
|
14
|
+
title = '',
|
|
15
|
+
}: IconProps) => {
|
|
16
|
+
return (
|
|
17
|
+
<svg
|
|
18
|
+
xmlns={name?.attributes && name?.attributes?.xmlns}
|
|
19
|
+
width={size}
|
|
20
|
+
height={size}
|
|
21
|
+
viewBox={name?.attributes && name?.attributes?.viewBox}
|
|
22
|
+
fill={name?.attributes?.fill || 'currentColor'}
|
|
23
|
+
stroke={color}
|
|
24
|
+
strokeWidth={name?.attributes['stroke-width']}
|
|
25
|
+
strokeLinecap={name?.attributes['stroke-linecap']}
|
|
26
|
+
strokeLinejoin={name?.attributes[' stroke-linejoin']}
|
|
27
|
+
className={className ? `icon ${className}` : 'icon'}
|
|
28
|
+
dangerouslySetInnerHTML={{
|
|
29
|
+
__html: title ? `<title>${title}</title>${name.content}` : name.content,
|
|
30
|
+
}}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default Icon;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Checkbox, Popup } from 'semantic-ui-react';
|
|
2
|
+
|
|
3
|
+
const QualityCheckToggle = ({ isEditMode, enabled, setEnabled }) => {
|
|
4
|
+
return (
|
|
5
|
+
<div className="quality-check-toggle">
|
|
6
|
+
<Popup
|
|
7
|
+
wide
|
|
8
|
+
basic
|
|
9
|
+
className="quality-check-popup"
|
|
10
|
+
content="Checks the AI's statements against cited sources to highlight possible inaccuracies and hallucinations."
|
|
11
|
+
trigger={
|
|
12
|
+
<Checkbox
|
|
13
|
+
id="fact-check-toggle"
|
|
14
|
+
toggle
|
|
15
|
+
label="Fact-check AI answer"
|
|
16
|
+
disabled={isEditMode}
|
|
17
|
+
checked={enabled}
|
|
18
|
+
onChange={() => setEnabled((v) => !v)}
|
|
19
|
+
/>
|
|
20
|
+
}
|
|
21
|
+
/>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default QualityCheckToggle;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { trackEvent } from '@eeacms/volto-matomo/utils';
|
|
2
|
+
|
|
3
|
+
const RelatedQuestions = ({
|
|
4
|
+
persona,
|
|
5
|
+
message,
|
|
6
|
+
isLoading,
|
|
7
|
+
onChoice,
|
|
8
|
+
enableMatomoTracking,
|
|
9
|
+
}) => {
|
|
10
|
+
const showRelatedQuestions = message.relatedQuestions?.length > 0;
|
|
11
|
+
|
|
12
|
+
const handleRelatedQuestionClick = (question) => {
|
|
13
|
+
if (!isLoading) {
|
|
14
|
+
if (enableMatomoTracking) {
|
|
15
|
+
trackEvent({
|
|
16
|
+
category: persona?.name ? `Chatbot - ${persona.name}` : 'Chatbot',
|
|
17
|
+
action: 'Chatbot: Related question click',
|
|
18
|
+
name: 'Message submitted',
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
onChoice(question);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<>
|
|
27
|
+
{showRelatedQuestions && (
|
|
28
|
+
<>
|
|
29
|
+
<h5>Related questions:</h5>
|
|
30
|
+
<div className="chat-related-questions">
|
|
31
|
+
{message.relatedQuestions.map(({ question }, idx) => (
|
|
32
|
+
<div
|
|
33
|
+
key={idx}
|
|
34
|
+
className="relatedQuestionButton"
|
|
35
|
+
role="button"
|
|
36
|
+
onClick={(e) => {
|
|
37
|
+
e.currentTarget.blur();
|
|
38
|
+
handleRelatedQuestionClick(question);
|
|
39
|
+
}}
|
|
40
|
+
onKeyDown={(e) => {
|
|
41
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
42
|
+
e.preventDefault(); // Prevent space from scrolling
|
|
43
|
+
e.currentTarget.blur();
|
|
44
|
+
handleRelatedQuestionClick(question);
|
|
45
|
+
}
|
|
46
|
+
}}
|
|
47
|
+
tabIndex="0"
|
|
48
|
+
>
|
|
49
|
+
{question}
|
|
50
|
+
</div>
|
|
51
|
+
))}
|
|
52
|
+
</div>
|
|
53
|
+
</>
|
|
54
|
+
)}
|
|
55
|
+
</>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export default RelatedQuestions;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Popup } from 'semantic-ui-react';
|
|
2
|
+
import SVGIcon from './Icon';
|
|
3
|
+
|
|
4
|
+
import FileIcon from '../../icons/file.svg';
|
|
5
|
+
import GlobeIcon from '../../icons/globe.svg';
|
|
6
|
+
|
|
7
|
+
import { injectLazyLibs } from '@plone/volto/helpers/Loadable/Loadable';
|
|
8
|
+
|
|
9
|
+
const SourceDetails_ = ({ source, index, luxon }) => {
|
|
10
|
+
// Ensure source is an object
|
|
11
|
+
if (!source || typeof source !== 'object') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { link, blurb, updated_at, source_type, semantic_identifier } = source;
|
|
16
|
+
const parsedDate = updated_at ? luxon.DateTime.fromISO(updated_at) : null;
|
|
17
|
+
const relativeTime = parsedDate?.toRelative();
|
|
18
|
+
const isLinkType = source_type === 'web';
|
|
19
|
+
const isDocumentType = source_type === 'file';
|
|
20
|
+
|
|
21
|
+
const renderIcon = () => {
|
|
22
|
+
if (isLinkType) {
|
|
23
|
+
return <SVGIcon name={GlobeIcon} size="16" alt="Web icon" />;
|
|
24
|
+
}
|
|
25
|
+
if (isDocumentType) {
|
|
26
|
+
return <SVGIcon name={FileIcon} size="16" alt="File icon" />;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const sourceContent = (
|
|
32
|
+
<>
|
|
33
|
+
{blurb && (
|
|
34
|
+
<div className="source-desc">
|
|
35
|
+
<span>{blurb}</span>
|
|
36
|
+
</div>
|
|
37
|
+
)}
|
|
38
|
+
{updated_at && (
|
|
39
|
+
<div className="source-date">
|
|
40
|
+
<span>{relativeTime}</span>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// Ensure we have valid data before rendering
|
|
47
|
+
if (!semantic_identifier) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{isLinkType && link ? (
|
|
54
|
+
<a
|
|
55
|
+
href={link}
|
|
56
|
+
rel="noreferrer"
|
|
57
|
+
target="_blank"
|
|
58
|
+
className="source source-link"
|
|
59
|
+
>
|
|
60
|
+
<div className="source-header">
|
|
61
|
+
<span className="chat-citation">{index}</span>
|
|
62
|
+
<div className="source-title" title={semantic_identifier}>
|
|
63
|
+
{semantic_identifier}
|
|
64
|
+
</div>
|
|
65
|
+
{renderIcon()}
|
|
66
|
+
</div>
|
|
67
|
+
{sourceContent}
|
|
68
|
+
</a>
|
|
69
|
+
) : (
|
|
70
|
+
<div className="source">
|
|
71
|
+
<div className="source-header">
|
|
72
|
+
<Popup
|
|
73
|
+
on="click"
|
|
74
|
+
wide="very"
|
|
75
|
+
content="This doc doesn't have a link."
|
|
76
|
+
trigger={<span className="chat-citation">{index}</span>}
|
|
77
|
+
popper={{ id: 'chat-citation-popup' }}
|
|
78
|
+
/>
|
|
79
|
+
<div className="source-title" title={semantic_identifier}>
|
|
80
|
+
{semantic_identifier}
|
|
81
|
+
</div>
|
|
82
|
+
{renderIcon()}
|
|
83
|
+
</div>
|
|
84
|
+
{sourceContent}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const SourceDetails = injectLazyLibs(['luxon'])(SourceDetails_);
|
|
92
|
+
|
|
93
|
+
export default SourceDetails;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface SourceChipProps {
|
|
4
|
+
icon?: React.ReactNode;
|
|
5
|
+
title: string;
|
|
6
|
+
onRemove?: () => void;
|
|
7
|
+
onClick?: () => void;
|
|
8
|
+
includeAnimation?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SourceChip({
|
|
12
|
+
icon,
|
|
13
|
+
title,
|
|
14
|
+
onRemove,
|
|
15
|
+
onClick,
|
|
16
|
+
includeAnimation,
|
|
17
|
+
}: SourceChipProps) {
|
|
18
|
+
const [isNew, setIsNew] = useState(true);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const timer = setTimeout(() => setIsNew(false), 300);
|
|
22
|
+
return () => clearTimeout(timer);
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
onClick={onClick}
|
|
28
|
+
className={`source-chip ${includeAnimation && isNew ? 'animate-in' : ''}`}
|
|
29
|
+
>
|
|
30
|
+
{icon && <div className="source-chip-icon">{icon}</div>}
|
|
31
|
+
<span>{title}</span>
|
|
32
|
+
{onRemove && (
|
|
33
|
+
<span
|
|
34
|
+
className="source-chip-remove"
|
|
35
|
+
onClick={(e) => {
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
onRemove();
|
|
38
|
+
}}
|
|
39
|
+
aria-label={`Remove ${title}`}
|
|
40
|
+
title={`Remove ${title}`}
|
|
41
|
+
role="button"
|
|
42
|
+
tabIndex={0}
|
|
43
|
+
onKeyDown={(e) => {
|
|
44
|
+
if (e.key === 'Enter') {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
onRemove();
|
|
47
|
+
}
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
✕
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import cx from 'classnames';
|
|
2
|
+
import { Button } from 'semantic-ui-react';
|
|
3
|
+
import { useCopyToClipboard } from '../utils';
|
|
4
|
+
import SVGIcon from './Icon';
|
|
5
|
+
import ChatMessageFeedback from './ChatMessageFeedback';
|
|
6
|
+
|
|
7
|
+
import CopyIcon from '../../icons/copy.svg';
|
|
8
|
+
import CheckIcon from '../../icons/check.svg';
|
|
9
|
+
|
|
10
|
+
const UserActionsToolbar = ({
|
|
11
|
+
className,
|
|
12
|
+
message,
|
|
13
|
+
enableFeedback,
|
|
14
|
+
feedbackReasons,
|
|
15
|
+
enableMatomoTracking,
|
|
16
|
+
persona,
|
|
17
|
+
}) => {
|
|
18
|
+
const [copied, handleCopy] = useCopyToClipboard(message?.message || '');
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={cx('message-actions', className)}>
|
|
22
|
+
<Button
|
|
23
|
+
basic
|
|
24
|
+
onClick={() => handleCopy()}
|
|
25
|
+
title="Copy"
|
|
26
|
+
aria-label="Copy"
|
|
27
|
+
disabled={copied}
|
|
28
|
+
>
|
|
29
|
+
{copied ? <SVGIcon name={CheckIcon} /> : <SVGIcon name={CopyIcon} />}
|
|
30
|
+
</Button>
|
|
31
|
+
|
|
32
|
+
{enableFeedback && (
|
|
33
|
+
<ChatMessageFeedback
|
|
34
|
+
message={message}
|
|
35
|
+
feedbackReasons={feedbackReasons}
|
|
36
|
+
enableMatomoTracking={enableMatomoTracking}
|
|
37
|
+
persona={persona}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default UserActionsToolbar;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import SVGIcon from './Icon';
|
|
3
|
+
import FileIcon from '../../icons/file.svg';
|
|
4
|
+
import GlobeIcon from '../../icons/globe.svg';
|
|
5
|
+
|
|
6
|
+
interface WebResultIconProps {
|
|
7
|
+
url: string;
|
|
8
|
+
size?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function WebResultIcon({ url, size = 10 }: WebResultIconProps) {
|
|
12
|
+
const [error, setError] = useState(false);
|
|
13
|
+
|
|
14
|
+
let hostname;
|
|
15
|
+
try {
|
|
16
|
+
hostname = new URL(url).hostname;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
// If URL parsing fails, fall back to FileIcon
|
|
19
|
+
return <SVGIcon name={FileIcon} size={size} />;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// If favicon failed to load, show GlobeIcon
|
|
23
|
+
if (error) {
|
|
24
|
+
return <SVGIcon name={GlobeIcon} size={size} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Use Google's advanced favicon service
|
|
28
|
+
return (
|
|
29
|
+
<img
|
|
30
|
+
src={`https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://${hostname}&size=128`}
|
|
31
|
+
alt="favicon"
|
|
32
|
+
height={size}
|
|
33
|
+
width={size}
|
|
34
|
+
onError={() => setError(true)}
|
|
35
|
+
style={{
|
|
36
|
+
height: `${size}px`,
|
|
37
|
+
width: `${size}px`,
|
|
38
|
+
objectFit: 'contain',
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Popup } from 'semantic-ui-react';
|
|
2
|
+
|
|
3
|
+
export function Citation({ link, index, value, message }) {
|
|
4
|
+
const isLinkType = value?.toString().startsWith('[');
|
|
5
|
+
|
|
6
|
+
const innerText = isLinkType
|
|
7
|
+
? value?.toString().split('[')[1].split(']')[0]
|
|
8
|
+
: index;
|
|
9
|
+
|
|
10
|
+
// New streaming architecture: use citations map to find document
|
|
11
|
+
const citationNum = parseInt(innerText);
|
|
12
|
+
const documentId = message?.citations?.[citationNum];
|
|
13
|
+
const document = documentId
|
|
14
|
+
? message?.documents?.find((doc) => doc.document_id === documentId)
|
|
15
|
+
: null;
|
|
16
|
+
|
|
17
|
+
const handleClick = (event) => {
|
|
18
|
+
if (link) {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
window.open(link, '_blank');
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const content = link ? (
|
|
25
|
+
<div>
|
|
26
|
+
<p>
|
|
27
|
+
<a href={link} tabIndex="-1" onClick={handleClick}>
|
|
28
|
+
{link}
|
|
29
|
+
</a>
|
|
30
|
+
</p>
|
|
31
|
+
|
|
32
|
+
{document?.match_highlights?.map((text, i) =>
|
|
33
|
+
text ? (
|
|
34
|
+
<p key={i} dangerouslySetInnerHTML={{ __html: `...${text}...` }} />
|
|
35
|
+
) : null,
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
) : (
|
|
39
|
+
<div>This doc doesn't have a link.</div>
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const popupContent = isLinkType ? (
|
|
43
|
+
content
|
|
44
|
+
) : (
|
|
45
|
+
<div>
|
|
46
|
+
{document?.match_highlights?.map((text, i) => (
|
|
47
|
+
<div key={i} dangerouslySetInnerHTML={{ __html: text }} />
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
{link ? (
|
|
55
|
+
<a href={link} tabIndex="-1" onClick={handleClick}>
|
|
56
|
+
<span className="chat-citation">{innerText}</span>
|
|
57
|
+
</a>
|
|
58
|
+
) : (
|
|
59
|
+
<Popup
|
|
60
|
+
on="click"
|
|
61
|
+
wide="very"
|
|
62
|
+
content={popupContent}
|
|
63
|
+
header={!isLinkType ? document.semantic_identifier : undefined}
|
|
64
|
+
trigger={<span className="chat-citation">{innerText}</span>}
|
|
65
|
+
popper={{ id: 'chat-citation-popup' }}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Modal, ModalContent, ModalHeader } from 'semantic-ui-react';
|
|
2
|
+
import cx from 'classnames';
|
|
3
|
+
import { convertToPercentage } from '../../utils';
|
|
4
|
+
import SVGIcon from '../Icon';
|
|
5
|
+
import { getSupportedBgColor } from './colors';
|
|
6
|
+
import { ClaimSegments } from './ClaimSegments';
|
|
7
|
+
|
|
8
|
+
import BotIcon from '../../../icons/bot.svg';
|
|
9
|
+
|
|
10
|
+
const stripHtml = (html) => {
|
|
11
|
+
const tmp = document.createElement('div');
|
|
12
|
+
tmp.innerHTML = html;
|
|
13
|
+
return tmp.textContent || tmp.innerText || '';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function stripMarkdown(md) {
|
|
17
|
+
return (
|
|
18
|
+
stripHtml(md)
|
|
19
|
+
.replace(/[`*_~>#-]/g, '') // formatting chars
|
|
20
|
+
.replace(/\n{2,}/g, '\n') // extra newlines
|
|
21
|
+
// [[1]](url) → <sup>1</sup>
|
|
22
|
+
.replace(/\[\[(\d+)\]\]\([^)]*\)/g, '<sup>$1</sup>')
|
|
23
|
+
// optional: strip normal markdown links [text](url) → text
|
|
24
|
+
.replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
|
|
25
|
+
.trim()
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const trimNonAlphanumeric = (str) =>
|
|
30
|
+
stripMarkdown(str).replace(/(?:^[^a-zA-Z0-9]+)|(?:[^a-zA-Z0-9]+$)/g, '');
|
|
31
|
+
|
|
32
|
+
export function ClaimModal({ claim, markers, text, citedSources }) {
|
|
33
|
+
const highlightText = trimNonAlphanumeric(text?.[0] || '');
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Modal
|
|
37
|
+
className={cx('claim-modal', getSupportedBgColor(claim.score, 'claim'))}
|
|
38
|
+
trigger={
|
|
39
|
+
<span
|
|
40
|
+
className={cx('claim', getSupportedBgColor(claim.score, 'claim'))}
|
|
41
|
+
>
|
|
42
|
+
{text}
|
|
43
|
+
</span>
|
|
44
|
+
}
|
|
45
|
+
>
|
|
46
|
+
<ModalHeader>
|
|
47
|
+
<div className="claim-modal-header">
|
|
48
|
+
<div className="claim-header-top">
|
|
49
|
+
<div className="circle assistant">
|
|
50
|
+
<SVGIcon name={BotIcon} size="20" color="white" />
|
|
51
|
+
</div>
|
|
52
|
+
<span className="claim-label">Verified Claim</span>
|
|
53
|
+
</div>
|
|
54
|
+
<blockquote className="claim-quote">
|
|
55
|
+
“
|
|
56
|
+
<span
|
|
57
|
+
dangerouslySetInnerHTML={{
|
|
58
|
+
__html: stripMarkdown(claim.claimString).replace(
|
|
59
|
+
highlightText,
|
|
60
|
+
`<b>${highlightText}</b>`,
|
|
61
|
+
),
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
”
|
|
65
|
+
</blockquote>
|
|
66
|
+
</div>
|
|
67
|
+
</ModalHeader>
|
|
68
|
+
<ModalContent>
|
|
69
|
+
<div className="claim-verification-card">
|
|
70
|
+
<div className="score-badge-section">
|
|
71
|
+
<div className="score-badge">
|
|
72
|
+
<span className="score-percentage">
|
|
73
|
+
{convertToPercentage(claim.score)}
|
|
74
|
+
</span>
|
|
75
|
+
<span className="score-label">Citation Support</span>
|
|
76
|
+
</div>
|
|
77
|
+
<div className="score-progress-bar">
|
|
78
|
+
<div
|
|
79
|
+
className="score-progress-fill"
|
|
80
|
+
style={{ width: `${claim.score * 100}%` }}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="rationale-section">
|
|
85
|
+
<h5 className="rationale-header">Rationale</h5>
|
|
86
|
+
<p className="claim-rationale">{claim.rationale}</p>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<ClaimSegments
|
|
91
|
+
segmentIds={claim.segmentIds}
|
|
92
|
+
segments={markers?.segments || {}}
|
|
93
|
+
citedSources={citedSources}
|
|
94
|
+
/>
|
|
95
|
+
</ModalContent>
|
|
96
|
+
</Modal>
|
|
97
|
+
);
|
|
98
|
+
}
|