@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.
Files changed (133) hide show
  1. package/.coverage.babel.config.js +9 -0
  2. package/.eslintrc.js +68 -0
  3. package/.husky/pre-commit +2 -0
  4. package/.release-it.json +17 -0
  5. package/AGENTS.md +89 -0
  6. package/CHANGELOG.md +770 -0
  7. package/DEVELOP.md +124 -0
  8. package/LICENSE.md +9 -0
  9. package/README.md +170 -0
  10. package/RELEASE.md +74 -0
  11. package/TESTING.md +5 -0
  12. package/babel.config.js +17 -0
  13. package/bootstrap +41 -0
  14. package/cypress.config.js +27 -0
  15. package/docker-compose.yml +32 -0
  16. package/jest-addon.config.js +465 -0
  17. package/jest.setup.js +65 -0
  18. package/locales/de/LC_MESSAGES/volto.po +14 -0
  19. package/locales/en/LC_MESSAGES/volto.po +14 -0
  20. package/locales/it/LC_MESSAGES/volto.po +14 -0
  21. package/locales/ro/LC_MESSAGES/volto.po +14 -0
  22. package/locales/volto.pot +16 -0
  23. package/package.json +98 -0
  24. package/razzle.extend.js +40 -0
  25. package/src/ChatBlock/ChatBlockEdit.jsx +46 -0
  26. package/src/ChatBlock/ChatBlockView.jsx +21 -0
  27. package/src/ChatBlock/chat/AIMessage.tsx +566 -0
  28. package/src/ChatBlock/chat/ChatMessage.tsx +35 -0
  29. package/src/ChatBlock/chat/ChatWindow.tsx +288 -0
  30. package/src/ChatBlock/chat/UserMessage.tsx +27 -0
  31. package/src/ChatBlock/chat/index.ts +4 -0
  32. package/src/ChatBlock/components/AutoResizeTextarea.jsx +67 -0
  33. package/src/ChatBlock/components/BlinkingDot.tsx +3 -0
  34. package/src/ChatBlock/components/ChatMessageFeedback.jsx +77 -0
  35. package/src/ChatBlock/components/EmptyState.jsx +70 -0
  36. package/src/ChatBlock/components/FeedbackModal.jsx +125 -0
  37. package/src/ChatBlock/components/HalloumiFeedback.jsx +126 -0
  38. package/src/ChatBlock/components/Icon.tsx +35 -0
  39. package/src/ChatBlock/components/QualityCheckToggle.jsx +26 -0
  40. package/src/ChatBlock/components/RelatedQuestions.jsx +59 -0
  41. package/src/ChatBlock/components/Source.jsx +93 -0
  42. package/src/ChatBlock/components/SourceChip.tsx +55 -0
  43. package/src/ChatBlock/components/Spinner.jsx +3 -0
  44. package/src/ChatBlock/components/UserActionsToolbar.jsx +44 -0
  45. package/src/ChatBlock/components/WebResultIcon.tsx +42 -0
  46. package/src/ChatBlock/components/markdown/Citation.jsx +70 -0
  47. package/src/ChatBlock/components/markdown/ClaimModal.jsx +98 -0
  48. package/src/ChatBlock/components/markdown/ClaimSegments.jsx +172 -0
  49. package/src/ChatBlock/components/markdown/RenderClaimView.jsx +96 -0
  50. package/src/ChatBlock/components/markdown/colors.js +29 -0
  51. package/src/ChatBlock/components/markdown/colors.less +52 -0
  52. package/src/ChatBlock/components/markdown/colors.test.js +69 -0
  53. package/src/ChatBlock/components/markdown/index.js +115 -0
  54. package/src/ChatBlock/fonts/DejaVuSans.ttf +0 -0
  55. package/src/ChatBlock/hocs/withOnyxData.jsx +46 -0
  56. package/src/ChatBlock/hooks/index.ts +7 -0
  57. package/src/ChatBlock/hooks/useChatController.ts +333 -0
  58. package/src/ChatBlock/hooks/useChatStreaming.ts +82 -0
  59. package/src/ChatBlock/hooks/useDeepCompareMemoize.js +17 -0
  60. package/src/ChatBlock/hooks/useMarked.js +44 -0
  61. package/src/ChatBlock/hooks/useQualityMarkers.js +119 -0
  62. package/src/ChatBlock/hooks/useScrollonStream.ts +131 -0
  63. package/src/ChatBlock/hooks/useToolDisplayTiming.ts +80 -0
  64. package/src/ChatBlock/index.js +32 -0
  65. package/src/ChatBlock/packets/MultiToolRenderer.tsx +235 -0
  66. package/src/ChatBlock/packets/RendererComponent.tsx +115 -0
  67. package/src/ChatBlock/packets/index.ts +4 -0
  68. package/src/ChatBlock/packets/renderers/CustomToolRenderer.tsx +63 -0
  69. package/src/ChatBlock/packets/renderers/FetchToolRenderer.tsx +59 -0
  70. package/src/ChatBlock/packets/renderers/ImageToolRenderer.tsx +62 -0
  71. package/src/ChatBlock/packets/renderers/MessageTextRenderer.tsx +172 -0
  72. package/src/ChatBlock/packets/renderers/ReasoningRenderer.tsx +122 -0
  73. package/src/ChatBlock/packets/renderers/SearchToolRenderer.tsx +323 -0
  74. package/src/ChatBlock/packets/renderers/index.ts +6 -0
  75. package/src/ChatBlock/schema.js +403 -0
  76. package/src/ChatBlock/services/index.ts +3 -0
  77. package/src/ChatBlock/services/messageProcessor.ts +348 -0
  78. package/src/ChatBlock/services/packetUtils.ts +48 -0
  79. package/src/ChatBlock/services/streamingService.ts +342 -0
  80. package/src/ChatBlock/style.less +1881 -0
  81. package/src/ChatBlock/tests/AIMessage.test.jsx +95 -0
  82. package/src/ChatBlock/tests/AutoResizeTextarea.test.jsx +49 -0
  83. package/src/ChatBlock/tests/BlinkingDot.test.jsx +71 -0
  84. package/src/ChatBlock/tests/ChatMessageFeedback.test.jsx +73 -0
  85. package/src/ChatBlock/tests/Citation.test.jsx +107 -0
  86. package/src/ChatBlock/tests/EmptyState.test.jsx +137 -0
  87. package/src/ChatBlock/tests/FeedbackModal.test.jsx +138 -0
  88. package/src/ChatBlock/tests/HalloumiFeedback.test.jsx +94 -0
  89. package/src/ChatBlock/tests/QualityCheckToggle.test.jsx +105 -0
  90. package/src/ChatBlock/tests/RelatedQuestions.test.jsx +215 -0
  91. package/src/ChatBlock/tests/Source.test.jsx +79 -0
  92. package/src/ChatBlock/tests/Spinner.test.jsx +18 -0
  93. package/src/ChatBlock/tests/index.test.js +51 -0
  94. package/src/ChatBlock/tests/messageProcessor.test.jsx +154 -0
  95. package/src/ChatBlock/tests/schema.test.js +166 -0
  96. package/src/ChatBlock/tests/useDeepCompareMemoize.test.js +107 -0
  97. package/src/ChatBlock/tests/useToolDisplayTiming.test.jsx +151 -0
  98. package/src/ChatBlock/types/cssmodules.d.ts +7 -0
  99. package/src/ChatBlock/types/interfaces.ts +154 -0
  100. package/src/ChatBlock/types/slate.d.ts +3 -0
  101. package/src/ChatBlock/types/streamingModels.ts +267 -0
  102. package/src/ChatBlock/types/volto.d.ts +3 -0
  103. package/src/ChatBlock/utils/citations.ts +25 -0
  104. package/src/ChatBlock/utils/index.tsx +114 -0
  105. package/src/halloumi/README.md +1 -0
  106. package/src/halloumi/generative.js +219 -0
  107. package/src/halloumi/generative.test.js +88 -0
  108. package/src/halloumi/middleware.js +70 -0
  109. package/src/halloumi/postprocessing.js +273 -0
  110. package/src/halloumi/postprocessing.test.js +441 -0
  111. package/src/halloumi/preprocessing.js +115 -0
  112. package/src/halloumi/preprocessing.test.js +245 -0
  113. package/src/icons/bot.svg +1 -0
  114. package/src/icons/check.svg +1 -0
  115. package/src/icons/chevron.svg +3 -0
  116. package/src/icons/clear.svg +1 -0
  117. package/src/icons/copy.svg +1 -0
  118. package/src/icons/done.svg +5 -0
  119. package/src/icons/external-link.svg +1 -0
  120. package/src/icons/file.svg +1 -0
  121. package/src/icons/glasses.svg +1 -0
  122. package/src/icons/globe.svg +1 -0
  123. package/src/icons/rotate.svg +1 -0
  124. package/src/icons/search.svg +5 -0
  125. package/src/icons/send.svg +1 -0
  126. package/src/icons/square-pen.svg +1 -0
  127. package/src/icons/stop.svg +9 -0
  128. package/src/icons/thumbs-down.svg +1 -0
  129. package/src/icons/thumbs-up.svg +1 -0
  130. package/src/icons/user.svg +1 -0
  131. package/src/index.js +58 -0
  132. package/src/middleware.js +250 -0
  133. 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,3 @@
1
+ export default function Spinner() {
2
+ return <div className="spinner"></div>;
3
+ }
@@ -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
+ &ldquo;
56
+ <span
57
+ dangerouslySetInnerHTML={{
58
+ __html: stripMarkdown(claim.claimString).replace(
59
+ highlightText,
60
+ `<b>${highlightText}</b>`,
61
+ ),
62
+ }}
63
+ />
64
+ &rdquo;
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
+ }