@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,172 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Tab, TabPane } from 'semantic-ui-react';
|
|
3
|
+
import SVGIcon from '../Icon';
|
|
4
|
+
import { RenderClaimView } from './RenderClaimView';
|
|
5
|
+
import LinkIcon from '../../../icons/external-link.svg';
|
|
6
|
+
import FileIcon from '../../../icons/file.svg';
|
|
7
|
+
import GlobeIcon from '../../../icons/globe.svg';
|
|
8
|
+
|
|
9
|
+
const VISIBLE_SEGMENTS = 50; // Number of citations to show by default
|
|
10
|
+
|
|
11
|
+
export function ClaimSegments({ segmentIds, segments, citedSources }) {
|
|
12
|
+
const joinedSources = citedSources.reduce((acc, source) => {
|
|
13
|
+
source.startIndex = acc.length ? acc.length + 1 : 0;
|
|
14
|
+
const sep = acc ? '\n' : '';
|
|
15
|
+
return acc + sep + source.halloumiContext; // + '\n---\n';
|
|
16
|
+
}, '');
|
|
17
|
+
|
|
18
|
+
const snippets = (segmentIds || [])
|
|
19
|
+
.map((id) => {
|
|
20
|
+
const segment = segments[id];
|
|
21
|
+
if (!segment) {
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.warn(`Could not find segment ${id} in `, segments);
|
|
24
|
+
}
|
|
25
|
+
return segment;
|
|
26
|
+
})
|
|
27
|
+
.filter((segment) => !!segment)
|
|
28
|
+
.map((segment) => {
|
|
29
|
+
const startOffset = Math.max(0, segment.startOffset); // sometimes startOffset comes as -1
|
|
30
|
+
const endOffset = segment.endOffset;
|
|
31
|
+
const text = joinedSources.slice(startOffset, endOffset);
|
|
32
|
+
const source = citedSources.find(
|
|
33
|
+
(source) =>
|
|
34
|
+
startOffset >= source.startIndex &&
|
|
35
|
+
endOffset <= source.halloumiContext.length + source.startIndex,
|
|
36
|
+
);
|
|
37
|
+
return {
|
|
38
|
+
...segment,
|
|
39
|
+
text,
|
|
40
|
+
source_id: source?.id,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const sourcesWithSnippets = citedSources
|
|
45
|
+
.map((source) => ({
|
|
46
|
+
...source,
|
|
47
|
+
snippets: snippets.filter((s) => s.source_id === source.id),
|
|
48
|
+
}))
|
|
49
|
+
.filter((source) => source.snippets.length > 0)
|
|
50
|
+
.sort((sa, sb) => sa.index - sb.index);
|
|
51
|
+
|
|
52
|
+
const [activeTab, setActiveTab] = React.useState(0);
|
|
53
|
+
const [visibleSegmentId, setVisibleSegment] = React.useState();
|
|
54
|
+
const [showAllButtons, setShowAllButtons] = React.useState(false);
|
|
55
|
+
|
|
56
|
+
const segmentContainerRef = React.useRef(null);
|
|
57
|
+
const spanRefs = React.useRef({});
|
|
58
|
+
|
|
59
|
+
const panes = sourcesWithSnippets.map((source, i) => {
|
|
60
|
+
const snippetButtons = source.snippets || [];
|
|
61
|
+
|
|
62
|
+
const segmentButtons = showAllButtons
|
|
63
|
+
? snippetButtons
|
|
64
|
+
: snippetButtons.slice(0, VISIBLE_SEGMENTS);
|
|
65
|
+
|
|
66
|
+
const sourceType = source.source_type;
|
|
67
|
+
const SourceIcon = source.source_type === 'web' ? GlobeIcon : FileIcon;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
menuItem: {
|
|
71
|
+
key: i,
|
|
72
|
+
content: (
|
|
73
|
+
<span title={source?.semantic_identifier}>
|
|
74
|
+
{source?.semantic_identifier}
|
|
75
|
+
</span>
|
|
76
|
+
),
|
|
77
|
+
className: `${activeTab === i ? 'active' : ''}`,
|
|
78
|
+
onClick: () => {
|
|
79
|
+
setActiveTab(i);
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
render: () => (
|
|
83
|
+
<TabPane>
|
|
84
|
+
<div className="source-card-header">
|
|
85
|
+
<div className="source-card-info">
|
|
86
|
+
<SVGIcon name={SourceIcon} size="20" className="source-icon" />
|
|
87
|
+
<div className="source-card-details">
|
|
88
|
+
<h5 className="source-card-title">
|
|
89
|
+
{source?.semantic_identifier}
|
|
90
|
+
</h5>
|
|
91
|
+
<span className="source-type-badge">{sourceType}</span>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
{source?.link && (
|
|
95
|
+
<a
|
|
96
|
+
href={source.link}
|
|
97
|
+
rel="noreferrer"
|
|
98
|
+
target="_blank"
|
|
99
|
+
className="source-external-link"
|
|
100
|
+
title="Open source"
|
|
101
|
+
>
|
|
102
|
+
<SVGIcon name={LinkIcon} size="16" />
|
|
103
|
+
</a>
|
|
104
|
+
)}
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className="citation-chips-section">
|
|
108
|
+
<h5 className="citation-chips-header">Jump to Citation</h5>
|
|
109
|
+
<div className="citation-chips-container">
|
|
110
|
+
{segmentButtons.map(({ id }) => (
|
|
111
|
+
<button
|
|
112
|
+
key={id}
|
|
113
|
+
className={`citation-chip ${
|
|
114
|
+
visibleSegmentId === id ? 'active' : ''
|
|
115
|
+
}`}
|
|
116
|
+
onClick={() => {
|
|
117
|
+
const container = segmentContainerRef.current;
|
|
118
|
+
const target = spanRefs.current[id];
|
|
119
|
+
if (container && target) {
|
|
120
|
+
const containerTop =
|
|
121
|
+
container.getBoundingClientRect().top;
|
|
122
|
+
const targetTop = target.getBoundingClientRect().top;
|
|
123
|
+
const scrollOffset =
|
|
124
|
+
targetTop - containerTop + container.scrollTop;
|
|
125
|
+
container.scrollTo({
|
|
126
|
+
top: scrollOffset - 50,
|
|
127
|
+
behavior: 'smooth',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
setVisibleSegment(id);
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
#{id}
|
|
134
|
+
</button>
|
|
135
|
+
))}
|
|
136
|
+
|
|
137
|
+
{snippetButtons.length > VISIBLE_SEGMENTS && (
|
|
138
|
+
<button
|
|
139
|
+
className="citation-chip more-chip"
|
|
140
|
+
onClick={() => setShowAllButtons(!showAllButtons)}
|
|
141
|
+
>
|
|
142
|
+
{showAllButtons
|
|
143
|
+
? 'Less'
|
|
144
|
+
: `+${snippetButtons.length - VISIBLE_SEGMENTS} More`}
|
|
145
|
+
</button>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
<RenderClaimView
|
|
150
|
+
contextText={joinedSources}
|
|
151
|
+
value={source.halloumiContext}
|
|
152
|
+
visibleSegmentId={visibleSegmentId}
|
|
153
|
+
segmentContainerRef={segmentContainerRef}
|
|
154
|
+
spanRefs={spanRefs}
|
|
155
|
+
sourceStartIndex={source.startIndex}
|
|
156
|
+
segments={source.snippets}
|
|
157
|
+
/>
|
|
158
|
+
</TabPane>
|
|
159
|
+
),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className="chat-window">
|
|
165
|
+
<Tab
|
|
166
|
+
menu={{ secondary: true, pointing: true }}
|
|
167
|
+
panes={panes}
|
|
168
|
+
activeIndex={activeTab}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
export const RenderClaimView = (props) => {
|
|
4
|
+
const {
|
|
5
|
+
value,
|
|
6
|
+
visibleSegmentId,
|
|
7
|
+
segmentContainerRef,
|
|
8
|
+
spanRefs,
|
|
9
|
+
segments = [],
|
|
10
|
+
sourceStartIndex = 0,
|
|
11
|
+
} = props;
|
|
12
|
+
|
|
13
|
+
const sortedSegments = [...segments].sort(
|
|
14
|
+
(a, b) => a.startOffset - b.startOffset,
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const parts = [];
|
|
18
|
+
let lastIndex = 0;
|
|
19
|
+
|
|
20
|
+
sortedSegments.forEach((segment) => {
|
|
21
|
+
const segmentStart = segment.startOffset - sourceStartIndex;
|
|
22
|
+
const segmentEnd = segment.endOffset - sourceStartIndex;
|
|
23
|
+
|
|
24
|
+
// Add the text part before the current segment
|
|
25
|
+
if (segmentStart > lastIndex) {
|
|
26
|
+
parts.push({
|
|
27
|
+
type: 'text',
|
|
28
|
+
content: value.slice(lastIndex, segmentStart),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Add the segment part
|
|
33
|
+
parts.push({
|
|
34
|
+
type: 'segment',
|
|
35
|
+
...segment,
|
|
36
|
+
content: value.slice(segmentStart, segmentEnd),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
lastIndex = segmentEnd;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Add the remaining text part after the last segment
|
|
43
|
+
if (lastIndex < value.length) {
|
|
44
|
+
parts.push({
|
|
45
|
+
type: 'text',
|
|
46
|
+
content: value.slice(lastIndex),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="citation-text-container" ref={segmentContainerRef}>
|
|
52
|
+
<div className="citation-text-content">
|
|
53
|
+
{parts.map((part, index) => {
|
|
54
|
+
const endline = part.content.endsWith('\n');
|
|
55
|
+
const content = part.content.split('\n');
|
|
56
|
+
|
|
57
|
+
if (part.type === 'segment') {
|
|
58
|
+
const isSelectedSegment = part.id === visibleSegmentId;
|
|
59
|
+
return (
|
|
60
|
+
<React.Fragment key={part.id || index}>
|
|
61
|
+
<span
|
|
62
|
+
ref={(el) => {
|
|
63
|
+
if (el) spanRefs.current[part.id] = el;
|
|
64
|
+
}}
|
|
65
|
+
className={`citation-segment ${
|
|
66
|
+
isSelectedSegment ? 'active' : ''
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
<mark className="citation-highlight">
|
|
70
|
+
{part.content.trim()}
|
|
71
|
+
<sup className="citation-ref">{part.id}</sup>
|
|
72
|
+
</mark>
|
|
73
|
+
{!endline && <> </>}
|
|
74
|
+
</span>
|
|
75
|
+
{endline && <span className="br" />}
|
|
76
|
+
</React.Fragment>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<React.Fragment key={index}>
|
|
82
|
+
{content
|
|
83
|
+
.filter((line) => !/^(DOCUMENT |Source)/.test(line))
|
|
84
|
+
.map((line, lineIndex) => (
|
|
85
|
+
<React.Fragment key={lineIndex}>
|
|
86
|
+
<span className="citation-line">{line}</span>
|
|
87
|
+
{lineIndex < content.length - 1 && <span className="br" />}
|
|
88
|
+
</React.Fragment>
|
|
89
|
+
))}
|
|
90
|
+
</React.Fragment>
|
|
91
|
+
);
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function getSupportedTextColor(score) {
|
|
2
|
+
if (0 <= score && score < 0.5) {
|
|
3
|
+
return 'text-red-500';
|
|
4
|
+
} else if (0.5 <= score && score <= 1) {
|
|
5
|
+
return 'text-green-500';
|
|
6
|
+
}
|
|
7
|
+
return 'text-gray-500';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getSupportedBgColor(score, prefix = 'bg') {
|
|
11
|
+
if (0 <= score && score < 0.125) {
|
|
12
|
+
return `${prefix}-red-500`;
|
|
13
|
+
} else if (0.125 <= score && score < 0.25) {
|
|
14
|
+
return `${prefix}-red-400`;
|
|
15
|
+
} else if (0.25 <= score && score < 0.375) {
|
|
16
|
+
return `${prefix}-red-300`;
|
|
17
|
+
} else if (0.375 <= score && score < 0.5) {
|
|
18
|
+
return `${prefix}-red-200`;
|
|
19
|
+
} else if (0.5 <= score && score < 0.625) {
|
|
20
|
+
return `${prefix}-green-200`;
|
|
21
|
+
} else if (0.625 <= score && score < 0.75) {
|
|
22
|
+
return `${prefix}-green-300`;
|
|
23
|
+
} else if (0.75 <= score && score < 0.875) {
|
|
24
|
+
return `${prefix}-green-400`;
|
|
25
|
+
} else if (0.875 <= score && score <= 1) {
|
|
26
|
+
return `${prefix}-green-500`;
|
|
27
|
+
}
|
|
28
|
+
return `${prefix}-gray-500`;
|
|
29
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
@gray-500: #6b7280;
|
|
2
|
+
@gray-400: #9ca3af;
|
|
3
|
+
@gray-300: #d1d5db;
|
|
4
|
+
@gray-200: #e5e7eb;
|
|
5
|
+
@gray-100: #f3f4f6;
|
|
6
|
+
@gray-50: #f9fafb;
|
|
7
|
+
|
|
8
|
+
@red-500: #ef4444;
|
|
9
|
+
@red-400: #f87171;
|
|
10
|
+
@red-300: #fca5a5;
|
|
11
|
+
@red-200: #fecaca;
|
|
12
|
+
@red-100: #fee2e2; //
|
|
13
|
+
|
|
14
|
+
@green-200: #bbf7d0;
|
|
15
|
+
@green-300: #86efac;
|
|
16
|
+
@green-400: #4ade80;
|
|
17
|
+
@green-500: #22c55e;
|
|
18
|
+
@green-100: #dcfce7; //
|
|
19
|
+
@green-700: #166534; //
|
|
20
|
+
|
|
21
|
+
@yellow-500: #eab308; //
|
|
22
|
+
@yellow-400: #facc15; //
|
|
23
|
+
@yellow-300: #fde047; //
|
|
24
|
+
@yellow-200: #fef08a; //
|
|
25
|
+
@yellow-100: #fef9c3; //
|
|
26
|
+
|
|
27
|
+
@amber-500: #f59e0b; //
|
|
28
|
+
@amber-100: #fef3c7; //
|
|
29
|
+
@amber-700: #92400e; //
|
|
30
|
+
|
|
31
|
+
@lime-500: #84cc16; //
|
|
32
|
+
|
|
33
|
+
.generate-colors(@namespace, @name, @color, @lighter-value: 20, @darker-value: 20) {
|
|
34
|
+
@var-name: ~'--@{namespace}-color';
|
|
35
|
+
|
|
36
|
+
.@{namespace}-@{name} {
|
|
37
|
+
// Fallback (Less compile-time)
|
|
38
|
+
--@{namespace}-color: @color;
|
|
39
|
+
--@{namespace}-lighter-color: lighten(@color, @lighter-value);
|
|
40
|
+
--@{namespace}-darker-color: darken(@color, @darker-value);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.generate-colors(claim, gray-500, @gray-500, 45, 20);
|
|
45
|
+
.generate-colors(claim, red-500, @red-500, 35, 20);
|
|
46
|
+
.generate-colors(claim, red-400, @red-400, 25, 25);
|
|
47
|
+
.generate-colors(claim, red-300, @red-300, 15, 25);
|
|
48
|
+
.generate-colors(claim, red-200, @red-200, 8, 30);
|
|
49
|
+
.generate-colors(claim, green-500, @green-500, 45, 20);
|
|
50
|
+
.generate-colors(claim, green-400, @green-400, 35, 25);
|
|
51
|
+
.generate-colors(claim, green-300, @green-300, 20, 35);
|
|
52
|
+
.generate-colors(claim, green-200, @green-200, 10, 45);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { getSupportedTextColor, getSupportedBgColor } from './colors';
|
|
2
|
+
|
|
3
|
+
describe('getSupportedTextColor', () => {
|
|
4
|
+
it('should return "text-red-500" for scores between 0 and 0.5', () => {
|
|
5
|
+
expect(getSupportedTextColor(0)).toBe('text-red-500');
|
|
6
|
+
expect(getSupportedTextColor(0.4)).toBe('text-red-500');
|
|
7
|
+
expect(getSupportedTextColor(0.499)).toBe('text-red-500');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('should return "text-green-500" for scores between 0.5 and 1', () => {
|
|
11
|
+
expect(getSupportedTextColor(0.5)).toBe('text-green-500');
|
|
12
|
+
expect(getSupportedTextColor(0.75)).toBe('text-green-500');
|
|
13
|
+
expect(getSupportedTextColor(1)).toBe('text-green-500');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return "text-gray-500" for invalid scores', () => {
|
|
17
|
+
expect(getSupportedTextColor(-1)).toBe('text-gray-500');
|
|
18
|
+
expect(getSupportedTextColor(1.5)).toBe('text-gray-500');
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('getSupportedBgColor', () => {
|
|
23
|
+
it('should return correct background colors based on score', () => {
|
|
24
|
+
// Test for scores in the range [0, 0.125)
|
|
25
|
+
expect(getSupportedBgColor(0)).toBe('bg-red-500');
|
|
26
|
+
expect(getSupportedBgColor(0.1)).toBe('bg-red-500');
|
|
27
|
+
expect(getSupportedBgColor(0.124)).toBe('bg-red-500');
|
|
28
|
+
|
|
29
|
+
// Test for scores in the range [0.125, 0.25)
|
|
30
|
+
expect(getSupportedBgColor(0.125)).toBe('bg-red-400');
|
|
31
|
+
expect(getSupportedBgColor(0.2)).toBe('bg-red-400');
|
|
32
|
+
expect(getSupportedBgColor(0.249)).toBe('bg-red-400');
|
|
33
|
+
|
|
34
|
+
// Test for scores in the range [0.25, 0.375)
|
|
35
|
+
expect(getSupportedBgColor(0.25)).toBe('bg-red-300');
|
|
36
|
+
expect(getSupportedBgColor(0.3)).toBe('bg-red-300');
|
|
37
|
+
expect(getSupportedBgColor(0.374)).toBe('bg-red-300');
|
|
38
|
+
|
|
39
|
+
// Test for scores in the range [0.375, 0.5)
|
|
40
|
+
expect(getSupportedBgColor(0.375)).toBe('bg-red-200');
|
|
41
|
+
expect(getSupportedBgColor(0.45)).toBe('bg-red-200');
|
|
42
|
+
expect(getSupportedBgColor(0.499)).toBe('bg-red-200');
|
|
43
|
+
|
|
44
|
+
// Test for scores in the range [0.5, 0.625)
|
|
45
|
+
expect(getSupportedBgColor(0.5)).toBe('bg-green-200');
|
|
46
|
+
expect(getSupportedBgColor(0.55)).toBe('bg-green-200');
|
|
47
|
+
expect(getSupportedBgColor(0.624)).toBe('bg-green-200');
|
|
48
|
+
|
|
49
|
+
// Test for scores in the range [0.625, 0.75)
|
|
50
|
+
expect(getSupportedBgColor(0.625)).toBe('bg-green-300');
|
|
51
|
+
expect(getSupportedBgColor(0.7)).toBe('bg-green-300');
|
|
52
|
+
expect(getSupportedBgColor(0.749)).toBe('bg-green-300');
|
|
53
|
+
|
|
54
|
+
// Test for scores in the range [0.75, 0.875)
|
|
55
|
+
expect(getSupportedBgColor(0.75)).toBe('bg-green-400');
|
|
56
|
+
expect(getSupportedBgColor(0.8)).toBe('bg-green-400');
|
|
57
|
+
expect(getSupportedBgColor(0.874)).toBe('bg-green-400');
|
|
58
|
+
|
|
59
|
+
// Test for scores in the range [0.875, 1]
|
|
60
|
+
expect(getSupportedBgColor(0.875)).toBe('bg-green-500');
|
|
61
|
+
expect(getSupportedBgColor(0.9)).toBe('bg-green-500');
|
|
62
|
+
expect(getSupportedBgColor(1)).toBe('bg-green-500');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return "bg-gray-500" for invalid scores', () => {
|
|
66
|
+
expect(getSupportedBgColor(-1)).toBe('bg-gray-500');
|
|
67
|
+
expect(getSupportedBgColor(1.1)).toBe('bg-gray-500');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ClaimModal } from './ClaimModal';
|
|
3
|
+
import { Citation } from './Citation';
|
|
4
|
+
import { transformEmailsToLinks } from '../../utils';
|
|
5
|
+
|
|
6
|
+
export function components(message, markers, citedSources) {
|
|
7
|
+
return {
|
|
8
|
+
table: (props) => {
|
|
9
|
+
const { node, children, ...rest } = props;
|
|
10
|
+
return (
|
|
11
|
+
<table className="ui celled table" {...rest}>
|
|
12
|
+
{children}
|
|
13
|
+
</table>
|
|
14
|
+
);
|
|
15
|
+
},
|
|
16
|
+
td: (props) => {
|
|
17
|
+
const { node, children, ...rest } = props;
|
|
18
|
+
// Process children to replace <br> strings with actual line breaks
|
|
19
|
+
const processedChildren = React.Children.map(children, (child) => {
|
|
20
|
+
if (typeof child === 'string' && child.includes('<br>')) {
|
|
21
|
+
// Split by <br> and insert actual <br /> elements
|
|
22
|
+
const parts = child.split('<br>');
|
|
23
|
+
return parts.reduce((acc, part, index) => {
|
|
24
|
+
acc.push(part);
|
|
25
|
+
if (index < parts.length - 1) {
|
|
26
|
+
acc.push(<br key={`br-${index}`} />);
|
|
27
|
+
}
|
|
28
|
+
return acc;
|
|
29
|
+
}, []);
|
|
30
|
+
}
|
|
31
|
+
return child;
|
|
32
|
+
});
|
|
33
|
+
return <td {...rest}>{processedChildren}</td>;
|
|
34
|
+
},
|
|
35
|
+
span: (props) => {
|
|
36
|
+
const { node, ...rest } = props;
|
|
37
|
+
const child = node.children[0];
|
|
38
|
+
let claim;
|
|
39
|
+
|
|
40
|
+
// identifies if the current text belongs to a claim
|
|
41
|
+
if (child.type === 'text' && child.position && markers) {
|
|
42
|
+
const start = child.position.start.offset;
|
|
43
|
+
const end = child.position.end.offset;
|
|
44
|
+
claim = markers.claims?.find(
|
|
45
|
+
(claim) =>
|
|
46
|
+
(start >= claim.startOffset && end <= claim.endOffset) ||
|
|
47
|
+
(claim.startOffset >= start && end <= claim.endOffset),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return !claim || claim?.score === null ? (
|
|
52
|
+
rest.children || []
|
|
53
|
+
) : (
|
|
54
|
+
<ClaimModal
|
|
55
|
+
claim={claim}
|
|
56
|
+
markers={markers}
|
|
57
|
+
text={rest.children}
|
|
58
|
+
citedSources={citedSources}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
},
|
|
62
|
+
a: (props) => {
|
|
63
|
+
const { node, children, href, ...rest } = props;
|
|
64
|
+
const value = children?.toString() || '';
|
|
65
|
+
|
|
66
|
+
// Check for blinking dot indicator
|
|
67
|
+
if (value?.startsWith('*')) {
|
|
68
|
+
return <div className="" />;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Check if this is a citation pattern [number]
|
|
72
|
+
if (value?.startsWith('[') && value?.endsWith(']')) {
|
|
73
|
+
const match = value.match(/\[(\d+)\]/);
|
|
74
|
+
if (match) {
|
|
75
|
+
// This is a citation - render Citation component
|
|
76
|
+
return (
|
|
77
|
+
<Citation link={href} value={value} message={message}>
|
|
78
|
+
{children}
|
|
79
|
+
</Citation>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Regular link - render normal anchor
|
|
85
|
+
const handleClick = (event) => {
|
|
86
|
+
if (href) {
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
window.open(href, '_blank');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<a href={href} onClick={handleClick} {...rest}>
|
|
94
|
+
{children}
|
|
95
|
+
</a>
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
p: ({ node, ...props }) => {
|
|
99
|
+
// TODO: reimplement this with rehype
|
|
100
|
+
const children = props.children;
|
|
101
|
+
const text = React.Children.map(children, (child) => {
|
|
102
|
+
if (typeof child === 'string') {
|
|
103
|
+
return transformEmailsToLinks(child);
|
|
104
|
+
}
|
|
105
|
+
return child;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<p {...props} className="text-default">
|
|
110
|
+
{text}
|
|
111
|
+
</p>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PlaceholderParagraph,
|
|
4
|
+
PlaceholderLine,
|
|
5
|
+
Placeholder,
|
|
6
|
+
} from 'semantic-ui-react';
|
|
7
|
+
|
|
8
|
+
const Loader = () => (
|
|
9
|
+
<Placeholder>
|
|
10
|
+
<PlaceholderParagraph>
|
|
11
|
+
<PlaceholderLine />
|
|
12
|
+
<PlaceholderLine />
|
|
13
|
+
<PlaceholderLine />
|
|
14
|
+
<PlaceholderLine />
|
|
15
|
+
</PlaceholderParagraph>
|
|
16
|
+
</Placeholder>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
export default function withOnyxData(callback) {
|
|
20
|
+
function wrapper(Component) {
|
|
21
|
+
function WrappedComponent(props) {
|
|
22
|
+
const [state, setState] = React.useState(null);
|
|
23
|
+
const [name, fetcher, depKey] = callback(props);
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
async function handler() {
|
|
27
|
+
if (fetcher) {
|
|
28
|
+
const response = await fetcher;
|
|
29
|
+
setState({ [name]: response.body });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
handler();
|
|
33
|
+
// the fetcher is not a stable function, but we depend on the relevant depKey
|
|
34
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
+
}, [depKey, name]);
|
|
36
|
+
|
|
37
|
+
return state ? (
|
|
38
|
+
<Component {...props} {...state} />
|
|
39
|
+
) : (
|
|
40
|
+
<Loader active={true} />
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return WrappedComponent;
|
|
44
|
+
}
|
|
45
|
+
return wrapper;
|
|
46
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { useToolDisplayTiming } from './useToolDisplayTiming';
|
|
2
|
+
export { useChatStreaming } from './useChatStreaming';
|
|
3
|
+
export { useChatController } from './useChatController';
|
|
4
|
+
export { useMarked } from './useMarked';
|
|
5
|
+
export { useQualityMarkers } from './useQualityMarkers';
|
|
6
|
+
export { useDeepCompareMemoize } from './useDeepCompareMemoize';
|
|
7
|
+
export { useScrollonStream } from './useScrollonStream';
|