@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,323 @@
1
+ import type {
2
+ SearchToolPacket,
3
+ SearchToolStart,
4
+ SearchToolDelta,
5
+ SectionEnd,
6
+ OnyxDocument,
7
+ } from '../../types/streamingModels';
8
+ import type { MessageRenderer } from '../../types/interfaces';
9
+ import { useEffect, useState, useRef, useMemo } from 'react';
10
+ import { PacketType } from '../../types/streamingModels';
11
+ import { SourceChip } from '../../components/SourceChip';
12
+ import { BlinkingDot } from '../../components/BlinkingDot';
13
+ import SVGIcon from '../../components/Icon';
14
+ import { WebResultIcon } from '../../components/WebResultIcon';
15
+ import SearchIcon from '../../../icons/search.svg';
16
+ import GlobeIcon from '../../../icons/globe.svg';
17
+ import FileIcon from '../../../icons/file.svg';
18
+
19
+ const INITIAL_RESULTS_TO_SHOW = 3;
20
+ const RESULTS_PER_EXPANSION = 10;
21
+
22
+ const INITIAL_QUERIES_TO_SHOW = 3;
23
+ const QUERIES_PER_EXPANSION = 5;
24
+
25
+ const SEARCHING_MIN_DURATION_MS = 1000; // 1 second minimum for "Searching" state
26
+ const SEARCHED_MIN_DURATION_MS = 1000; // 1 second minimum for "Searched" state
27
+
28
+ /**
29
+ * ResultIcon component that displays either a favicon for web results or a FileIcon for internal documents
30
+ */
31
+ const ResultIcon = ({
32
+ doc,
33
+ size,
34
+ isInternetSearch,
35
+ }: {
36
+ doc: OnyxDocument;
37
+ size: number;
38
+ isInternetSearch: boolean;
39
+ }) => {
40
+ // Check if this is a web/internet result
41
+ if (doc.link && (isInternetSearch || doc.source_type === 'web')) {
42
+ return <WebResultIcon url={doc.link} size={size} />;
43
+ }
44
+ // For internal documents without links, use FileIcon
45
+ return <SVGIcon name={FileIcon} size={size} />;
46
+ };
47
+
48
+ const constructCurrentSearchState = (
49
+ packets: SearchToolPacket[],
50
+ ): {
51
+ queries: string[];
52
+ results: OnyxDocument[];
53
+ isSearching: boolean;
54
+ isComplete: boolean;
55
+ isInternetSearch: boolean;
56
+ } => {
57
+ const searchStart = packets.find(
58
+ (packet) => packet.obj.type === PacketType.SEARCH_TOOL_START,
59
+ )?.obj as SearchToolStart | null;
60
+
61
+ const searchDeltas = packets
62
+ .filter((packet) => packet.obj.type === PacketType.SEARCH_TOOL_DELTA)
63
+ .map((packet) => packet.obj as SearchToolDelta);
64
+
65
+ const searchEnd = packets.find(
66
+ (packet) => packet.obj.type === PacketType.SECTION_END,
67
+ )?.obj as SectionEnd | null;
68
+
69
+ // Extract queries from ToolDelta packets
70
+ const queries = searchDeltas
71
+ .flatMap((delta) => delta?.queries || [])
72
+ .filter((query, index, arr) => arr.indexOf(query) === index); // Remove duplicates
73
+
74
+ const seenDocIds = new Set<string>();
75
+ const results = searchDeltas
76
+ .flatMap((delta) => delta?.documents || [])
77
+ .filter((doc) => {
78
+ if (!doc || !doc.document_id) return false;
79
+ if (seenDocIds.has(doc.document_id)) return false;
80
+ seenDocIds.add(doc.document_id);
81
+ return true;
82
+ });
83
+
84
+ const isSearching = Boolean(searchStart && !searchEnd);
85
+ const isComplete = Boolean(searchStart && searchEnd);
86
+ const isInternetSearch = searchStart?.is_internet_search || false;
87
+
88
+ return { queries, results, isSearching, isComplete, isInternetSearch };
89
+ };
90
+
91
+ export const SearchToolRenderer: MessageRenderer<SearchToolPacket> = ({
92
+ packets,
93
+ onComplete,
94
+ animate,
95
+ children,
96
+ }) => {
97
+ const { queries, results, isSearching, isComplete, isInternetSearch } =
98
+ constructCurrentSearchState(packets);
99
+
100
+ // Track search timing for minimum display duration
101
+ const [searchStartTime, setSearchStartTime] = useState<number | null>(null);
102
+ const [shouldShowAsSearching, setShouldShowAsSearching] = useState(false);
103
+ const [shouldShowAsSearched, setShouldShowAsSearched] = useState(isComplete);
104
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
105
+ const searchedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
106
+ const completionHandledRef = useRef(false);
107
+
108
+ // Track how many results to show
109
+ const [resultsToShow, setResultsToShow] = useState(INITIAL_RESULTS_TO_SHOW);
110
+
111
+ // Track how many queries to show
112
+ const [queriesToShow, setQueriesToShow] = useState(INITIAL_QUERIES_TO_SHOW);
113
+
114
+ // Track when search starts (even if the search completes instantly)
115
+ useEffect(() => {
116
+ if ((isSearching || isComplete) && searchStartTime === null) {
117
+ setSearchStartTime(Date.now());
118
+ setShouldShowAsSearching(true);
119
+ }
120
+ }, [isSearching, isComplete, searchStartTime]);
121
+
122
+ // Handle search completion with minimum duration
123
+ useEffect(() => {
124
+ if (
125
+ isComplete &&
126
+ searchStartTime !== null &&
127
+ !completionHandledRef.current
128
+ ) {
129
+ completionHandledRef.current = true;
130
+ const elapsedTime = Date.now() - searchStartTime;
131
+ const minimumSearchingDuration = animate ? SEARCHING_MIN_DURATION_MS : 0;
132
+ const minimumSearchedDuration = animate ? SEARCHED_MIN_DURATION_MS : 0;
133
+
134
+ const handleSearchingToSearched = () => {
135
+ setShouldShowAsSearching(false);
136
+ setShouldShowAsSearched(true);
137
+
138
+ searchedTimeoutRef.current = setTimeout(() => {
139
+ setShouldShowAsSearched(false);
140
+ onComplete();
141
+ }, minimumSearchedDuration);
142
+ };
143
+
144
+ if (elapsedTime >= minimumSearchingDuration) {
145
+ // Enough time has passed for searching, transition to searched immediately
146
+ handleSearchingToSearched();
147
+ } else {
148
+ // Not enough time has passed for searching, delay the transition
149
+ const remainingTime = minimumSearchingDuration - elapsedTime;
150
+ timeoutRef.current = setTimeout(
151
+ handleSearchingToSearched,
152
+ remainingTime,
153
+ );
154
+ }
155
+ }
156
+ }, [isComplete, searchStartTime, animate, onComplete]);
157
+
158
+ // Cleanup timeouts on unmount
159
+ useEffect(() => {
160
+ return () => {
161
+ if (timeoutRef.current) {
162
+ clearTimeout(timeoutRef.current);
163
+ }
164
+ if (searchedTimeoutRef.current) {
165
+ clearTimeout(searchedTimeoutRef.current);
166
+ }
167
+ };
168
+ }, []);
169
+
170
+ const status = useMemo(() => {
171
+ const searchType = isInternetSearch ? 'the web' : 'internal documents';
172
+
173
+ // If we have documents to show and we're in the searched state, show "Searched"
174
+ if (results.length > 0) {
175
+ // If we're still showing as searching (before transition), show "Searching"
176
+ if (shouldShowAsSearching) {
177
+ return `Searching ${searchType}`;
178
+ }
179
+ // Otherwise show "Searched"
180
+ return `Searched ${searchType}`;
181
+ }
182
+
183
+ // Handle states based on timing
184
+ if (shouldShowAsSearched) {
185
+ return `Searched ${searchType}`;
186
+ }
187
+ if (isSearching || isComplete || shouldShowAsSearching) {
188
+ return `Searching ${searchType}`;
189
+ }
190
+ return null;
191
+ }, [
192
+ isSearching,
193
+ isComplete,
194
+ shouldShowAsSearching,
195
+ shouldShowAsSearched,
196
+ results.length,
197
+ isInternetSearch,
198
+ ]);
199
+
200
+ // Determine the icon based on search type
201
+ const IconComponent = ({ size }: { size: number }) => (
202
+ <SVGIcon name={isInternetSearch ? GlobeIcon : SearchIcon} size={size} />
203
+ );
204
+
205
+ // Don't render anything if search hasn't started
206
+ if (queries.length === 0) {
207
+ return children({
208
+ icon: IconComponent,
209
+ status: status,
210
+ content: <div></div>,
211
+ });
212
+ }
213
+
214
+ return children({
215
+ icon: IconComponent,
216
+ status,
217
+ content: (
218
+ <div className="search-tool-renderer">
219
+ <div className="queries-section">
220
+ <div className="queries-header">
221
+ <strong>Queries</strong>
222
+ </div>
223
+ <div className="queries-list">
224
+ {queries.slice(0, queriesToShow).map((query, index) => (
225
+ <div
226
+ key={index}
227
+ className="query-item"
228
+ style={{
229
+ animationDelay: `${index * 30}ms`,
230
+ }}
231
+ >
232
+ <SourceChip
233
+ icon={<SVGIcon name={SearchIcon} size={16} />}
234
+ title={query}
235
+ />
236
+ </div>
237
+ ))}
238
+ {queries.length > queriesToShow && (
239
+ <div
240
+ className="query-item more-button"
241
+ style={{
242
+ animationDelay: `${queriesToShow * 30}ms`,
243
+ }}
244
+ >
245
+ <SourceChip
246
+ title={`${queries.length - queriesToShow} more...`}
247
+ onClick={() => {
248
+ setQueriesToShow((prevQueries) =>
249
+ Math.min(
250
+ prevQueries + QUERIES_PER_EXPANSION,
251
+ queries.length,
252
+ ),
253
+ );
254
+ }}
255
+ />
256
+ </div>
257
+ )}
258
+ </div>
259
+ </div>
260
+
261
+ {queries.length === 0 && <BlinkingDot />}
262
+
263
+ <div className="results-section">
264
+ <div className="results-header">
265
+ <strong>{isInternetSearch ? 'Results' : 'Documents'}</strong>
266
+ </div>
267
+
268
+ <div className="results-list">
269
+ {results.slice(0, resultsToShow).map((result, index) => (
270
+ <div
271
+ key={result.document_id}
272
+ className="result-item"
273
+ style={{
274
+ animationDelay: `${index * 30}ms`,
275
+ }}
276
+ >
277
+ <SourceChip
278
+ icon={
279
+ <ResultIcon
280
+ doc={result}
281
+ size={16}
282
+ isInternetSearch={isInternetSearch}
283
+ />
284
+ }
285
+ title={result.semantic_identifier || ''}
286
+ onClick={() => {
287
+ if (result.link) {
288
+ window.open(result.link, '_blank');
289
+ }
290
+ }}
291
+ />
292
+ </div>
293
+ ))}
294
+ {results.length > resultsToShow && (
295
+ <div
296
+ className="result-item more-button"
297
+ style={{
298
+ animationDelay: `${
299
+ Math.min(resultsToShow, results.length) * 30
300
+ }ms`,
301
+ }}
302
+ >
303
+ <SourceChip
304
+ title={`${results.length - resultsToShow} more...`}
305
+ onClick={() => {
306
+ setResultsToShow((prevResults) =>
307
+ Math.min(
308
+ prevResults + RESULTS_PER_EXPANSION,
309
+ results.length,
310
+ ),
311
+ );
312
+ }}
313
+ />
314
+ </div>
315
+ )}
316
+
317
+ {results.length === 0 && queries.length > 0 && <BlinkingDot />}
318
+ </div>
319
+ </div>
320
+ </div>
321
+ ),
322
+ });
323
+ };
@@ -0,0 +1,6 @@
1
+ export { MessageTextRenderer } from './MessageTextRenderer';
2
+ export { SearchToolRenderer } from './SearchToolRenderer';
3
+ export { ReasoningRenderer } from './ReasoningRenderer';
4
+ export { CustomToolRenderer } from './CustomToolRenderer';
5
+ export { ImageToolRenderer } from './ImageToolRenderer';
6
+ export { FetchToolRenderer } from './FetchToolRenderer';