@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,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 && <>&nbsp;</>}
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
+ }
@@ -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';