@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,333 @@
1
+ import type { Message } from '../types/interfaces';
2
+ import { useState, useCallback, useRef, useEffect } from 'react';
3
+ import { useChatStreaming } from './useChatStreaming';
4
+ import { createChatSession, sendMessage } from '../services/streamingService';
5
+ import { PacketType } from '../types/streamingModels';
6
+ import { ResearchType } from '../types/interfaces';
7
+
8
+ interface UseChatControllerProps {
9
+ personaId: number;
10
+ enableQgen?: boolean;
11
+ qgenAsistantId?: number;
12
+ deepResearch?: string;
13
+ }
14
+
15
+ interface RelatedQuestion {
16
+ question: string;
17
+ }
18
+
19
+ // Extract JSON array from related questions response
20
+ function extractRelatedQuestions(str: string): RelatedQuestion[] {
21
+ const regex = /\[[\s\S]*?\]/;
22
+ const match = str.match(regex);
23
+
24
+ if (match) {
25
+ try {
26
+ return JSON.parse(match[0]);
27
+ } catch {
28
+ // Fallback to line-by-line parsing
29
+ return str
30
+ .split('\n')
31
+ .filter((line) => line.trim())
32
+ .map((question) => ({ question }));
33
+ }
34
+ }
35
+
36
+ return str
37
+ .split('\n')
38
+ .filter((line) => line.trim())
39
+ .map((question) => ({ question }));
40
+ }
41
+
42
+ // Fetch related questions using the qgen assistant
43
+ async function fetchRelatedQuestions(
44
+ query: string,
45
+ answer: string,
46
+ qgenAsistantId: number,
47
+ ): Promise<RelatedQuestion[]> {
48
+ try {
49
+ const chatSessionId = await createChatSession(
50
+ qgenAsistantId,
51
+ `Q: ${query}`,
52
+ );
53
+
54
+ const params = {
55
+ message: `Question: ${query}\nAnswer:\n${answer}`,
56
+ alternateAssistantId: qgenAsistantId,
57
+ fileDescriptors: [],
58
+ parentMessageId: null,
59
+ chatSessionId,
60
+ promptId: 0,
61
+ filters: null,
62
+ selectedDocumentIds: [],
63
+ use_agentic_search: false,
64
+ regenerate: false,
65
+ };
66
+
67
+ let result = '';
68
+ for await (const packets of sendMessage(params)) {
69
+ for (const packet of packets) {
70
+ if (packet.obj.type === PacketType.MESSAGE_DELTA) {
71
+ result += packet.obj.content;
72
+ }
73
+ }
74
+ }
75
+
76
+ return extractRelatedQuestions(result);
77
+ } catch (error) {
78
+ console.error('Error fetching related questions:', error);
79
+ return [];
80
+ }
81
+ }
82
+
83
+ export function useChatController({
84
+ personaId,
85
+ enableQgen = false,
86
+ qgenAsistantId,
87
+ deepResearch,
88
+ }: UseChatControllerProps) {
89
+ const [messages, setMessages] = useState<Message[]>([]);
90
+ const [chatSessionId, setChatSessionId] = useState<string | null>(null);
91
+ const [isDeepResearchEnabled, setIsDeepResearchEnabled] = useState(
92
+ deepResearch === 'always_on' || deepResearch === 'user_on',
93
+ );
94
+ const [isFetchingRelatedQuestions, setIsFetchingRelatedQuestions] =
95
+ useState(false);
96
+ const [isCancelled, setIsCancelled] = useState(false);
97
+
98
+ const messagesRef = useRef(messages);
99
+ const nodeIdCounter = useRef(1);
100
+ const isCancelledRef = useRef(isCancelled);
101
+
102
+ // Keep ref in sync with state
103
+ useEffect(() => {
104
+ isCancelledRef.current = isCancelled;
105
+ }, [isCancelled]);
106
+
107
+ const { isStreaming, startStreaming, cancelStreaming } = useChatStreaming({
108
+ onMessageUpdate: (message) => {
109
+ setMessages((prev) => {
110
+ const existingIndex = prev.findIndex(
111
+ (m) => m.nodeId === message.nodeId,
112
+ );
113
+
114
+ if (existingIndex >= 0) {
115
+ const updated = [...prev];
116
+ updated[existingIndex] = { ...message };
117
+ return updated;
118
+ }
119
+ messagesRef.current = [...prev, message];
120
+ return messagesRef.current;
121
+ });
122
+ },
123
+ onComplete: (completedMessage, processor) => {
124
+ // Get real database IDs from backend
125
+ const { userMessageId, assistantMessageId } = processor.messageIds;
126
+
127
+ // Update messages with real IDs
128
+ if (userMessageId || assistantMessageId) {
129
+ setMessages((prev) => {
130
+ const updatedMessages = prev.map((msg) => {
131
+ // Update user message with real ID
132
+ if (
133
+ userMessageId &&
134
+ msg.type === 'user' &&
135
+ msg.nodeId === completedMessage.parentNodeId
136
+ ) {
137
+ return { ...msg, messageId: userMessageId };
138
+ }
139
+
140
+ // Update assistant message with real ID
141
+ if (
142
+ assistantMessageId &&
143
+ msg.type === 'assistant' &&
144
+ msg.nodeId === completedMessage.nodeId
145
+ ) {
146
+ return { ...msg, messageId: assistantMessageId };
147
+ }
148
+
149
+ return msg;
150
+ });
151
+ messagesRef.current = updatedMessages;
152
+ return updatedMessages;
153
+ });
154
+ }
155
+ },
156
+ onError: (error) => {
157
+ const errorMessage: Message = {
158
+ messageId: Date.now(),
159
+ nodeId: nodeIdCounter.current++,
160
+ message: '',
161
+ error: `Error: ${error.message}`,
162
+ type: 'error',
163
+ parentNodeId:
164
+ messages.length > 0 ? messages[messages.length - 1].nodeId : null,
165
+ packets: [],
166
+ groupedPackets: [],
167
+ toolPackets: [],
168
+ displayPackets: [],
169
+ files: [],
170
+ toolCall: null,
171
+ };
172
+
173
+ setMessages((prev) => [...prev, errorMessage]);
174
+ },
175
+ });
176
+
177
+ const onSubmit = useCallback(
178
+ async ({ message }: { message?: string }) => {
179
+ if (isStreaming) return;
180
+
181
+ try {
182
+ // Create session if needed
183
+ let sessionId = chatSessionId;
184
+
185
+ if (!sessionId) {
186
+ sessionId = await createChatSession(personaId, 'Chat session');
187
+ setChatSessionId(sessionId);
188
+ }
189
+
190
+ let messageText = message;
191
+ let parentNodeId: number | null = null;
192
+ let parentMessageId: number | null = null;
193
+
194
+ // For new messages, set parent to the last assistant message
195
+ const lastMessage = messages
196
+ .filter((m) => m.type === 'assistant')
197
+ .pop();
198
+
199
+ const lastPacket = lastMessage?.packets.pop();
200
+
201
+ if (lastMessage && lastPacket?.obj.type !== PacketType.ERROR) {
202
+ parentNodeId = lastMessage.nodeId;
203
+ parentMessageId = lastMessage.messageId || null;
204
+ }
205
+
206
+ if (!messageText?.trim()) return;
207
+
208
+ // Add user message
209
+ const userNodeId = nodeIdCounter.current++;
210
+ const userMessage: Message = {
211
+ messageId: Date.now(),
212
+ nodeId: userNodeId,
213
+ message: messageText,
214
+ type: 'user',
215
+ parentNodeId,
216
+ packets: [],
217
+ time_sent: new Date().toISOString(),
218
+ files: [],
219
+ toolCall: null,
220
+ researchType: isDeepResearchEnabled
221
+ ? ResearchType.Deep
222
+ : ResearchType.Fast,
223
+ };
224
+
225
+ setMessages((prev) => [...prev, userMessage]);
226
+
227
+ // Start streaming assistant response
228
+ const assistantNodeId = nodeIdCounter.current++;
229
+ await startStreaming(
230
+ {
231
+ message: messageText,
232
+ chatSessionId: sessionId,
233
+ parentMessageId: parentMessageId || null,
234
+ useAgentSearch: isDeepResearchEnabled,
235
+ regenerate: false,
236
+ filters: null,
237
+ selectedDocumentIds: [],
238
+ },
239
+ assistantNodeId,
240
+ userNodeId,
241
+ );
242
+ } catch (error) {
243
+ console.error('Failed to submit message:', error);
244
+ }
245
+ },
246
+ [
247
+ chatSessionId,
248
+ personaId,
249
+ isStreaming,
250
+ isDeepResearchEnabled,
251
+ startStreaming,
252
+ messages,
253
+ ],
254
+ );
255
+
256
+ const onFetchRelatedQuestions = useCallback(async () => {
257
+ const latestAssistantMessage = messages
258
+ .filter((m) => m.type === 'assistant')
259
+ .pop();
260
+
261
+ if (
262
+ enableQgen &&
263
+ qgenAsistantId &&
264
+ latestAssistantMessage?.type === 'assistant'
265
+ ) {
266
+ if (isDeepResearchEnabled) {
267
+ setMessages((prev) => {
268
+ return prev.map((m) =>
269
+ m.nodeId === latestAssistantMessage.nodeId
270
+ ? { ...m, relatedQuestions: null }
271
+ : m,
272
+ );
273
+ });
274
+ return;
275
+ }
276
+
277
+ let relatedQuestions: RelatedQuestion[] | null = null;
278
+ setIsFetchingRelatedQuestions(true);
279
+
280
+ try {
281
+ // Get the parent user message directly from the latest assistant message
282
+ const userMessage = messages.find(
283
+ (m) => m.nodeId === latestAssistantMessage.parentNodeId,
284
+ );
285
+
286
+ if (userMessage && latestAssistantMessage.message) {
287
+ relatedQuestions = await fetchRelatedQuestions(
288
+ userMessage.message,
289
+ latestAssistantMessage.message,
290
+ qgenAsistantId,
291
+ );
292
+ }
293
+ } catch (error) {
294
+ console.error('Failed to fetch related questions:', error);
295
+ } finally {
296
+ setMessages((prev) => {
297
+ return prev.map((m) =>
298
+ m.nodeId === latestAssistantMessage.nodeId
299
+ ? { ...m, relatedQuestions }
300
+ : m,
301
+ );
302
+ });
303
+ setIsFetchingRelatedQuestions(false);
304
+ }
305
+ }
306
+ }, [messages, enableQgen, qgenAsistantId, isDeepResearchEnabled]);
307
+
308
+ const clearChat = useCallback(() => {
309
+ setMessages([]);
310
+ setChatSessionId(null);
311
+ nodeIdCounter.current = 1;
312
+ setIsCancelled(false);
313
+ }, []);
314
+
315
+ const handleCancel = useCallback(() => {
316
+ setIsCancelled(true);
317
+ cancelStreaming();
318
+ }, [cancelStreaming]);
319
+
320
+ return {
321
+ messages,
322
+ isStreaming,
323
+ isCancelled,
324
+ isFetchingRelatedQuestions,
325
+ onSubmit,
326
+ onFetchRelatedQuestions,
327
+ clearChat,
328
+ cancelStreaming: handleCancel,
329
+ isDeepResearchEnabled,
330
+ setIsDeepResearchEnabled,
331
+ chatSessionId,
332
+ };
333
+ }
@@ -0,0 +1,82 @@
1
+ import type { Message } from '../types/interfaces';
2
+ import type { SendMessageParams } from '../services/streamingService';
3
+ import { useState, useCallback, useRef } from 'react';
4
+ import { sendMessage } from '../services/streamingService';
5
+ import { MessageProcessor } from '../services/messageProcessor';
6
+
7
+ interface UseChatStreamingProps {
8
+ onMessageUpdate?: (message: Message, processor: MessageProcessor) => void;
9
+ onComplete?: (message: Message, processor: MessageProcessor) => void;
10
+ onError?: (error: Error, processor: MessageProcessor) => void;
11
+ }
12
+
13
+ export function useChatStreaming({
14
+ onMessageUpdate,
15
+ onComplete,
16
+ onError,
17
+ }: UseChatStreamingProps = {}) {
18
+ const [isStreaming, setIsStreaming] = useState(false);
19
+ const [currentMessage, setCurrentMessage] = useState<Message | null>(null);
20
+ const abortControllerRef = useRef<AbortController | null>(null);
21
+ const processorRef = useRef<MessageProcessor | null>(null);
22
+
23
+ const startStreaming = useCallback(
24
+ async (
25
+ params: SendMessageParams,
26
+ nodeId: number,
27
+ parentNodeId: number | null,
28
+ ) => {
29
+ // Cancel any existing streaming
30
+ if (abortControllerRef.current) {
31
+ abortControllerRef.current.abort();
32
+ }
33
+
34
+ // Reset previous processor state
35
+ setIsStreaming(true);
36
+ abortControllerRef.current = new AbortController();
37
+ processorRef.current = new MessageProcessor(nodeId, parentNodeId);
38
+
39
+ try {
40
+ for await (const packets of sendMessage({
41
+ ...params,
42
+ signal: abortControllerRef.current.signal,
43
+ })) {
44
+ processorRef.current.addPackets(packets);
45
+ const message = processorRef.current.getMessage();
46
+
47
+ setCurrentMessage(message);
48
+ onMessageUpdate?.(message, processorRef.current);
49
+
50
+ if (processorRef.current.isComplete) {
51
+ onComplete?.(message, processorRef.current);
52
+ break;
53
+ }
54
+ }
55
+ } catch (error) {
56
+ if ((error as Error).name !== 'AbortError') {
57
+ console.error('Streaming error:', error);
58
+ onError?.(error as Error, processorRef.current);
59
+ }
60
+ } finally {
61
+ setIsStreaming(false);
62
+ abortControllerRef.current = null;
63
+ processorRef.current = null;
64
+ }
65
+ },
66
+ [onMessageUpdate, onComplete, onError],
67
+ );
68
+
69
+ const cancelStreaming = useCallback(() => {
70
+ if (abortControllerRef.current) {
71
+ abortControllerRef.current.abort();
72
+ setIsStreaming(false);
73
+ }
74
+ }, []);
75
+
76
+ return {
77
+ isStreaming,
78
+ currentMessage,
79
+ startStreaming,
80
+ cancelStreaming,
81
+ };
82
+ }
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { dequal } from 'dequal';
3
+
4
+ export function useDeepCompareMemoize(dependencies) {
5
+ const dependenciesRef = React.useRef(dependencies);
6
+ const signalRef = React.useRef(0);
7
+
8
+ if (!dequal(dependencies, dependenciesRef.current)) {
9
+ dependenciesRef.current = dependencies;
10
+ signalRef.current += 1;
11
+ }
12
+
13
+ const currentSignal = signalRef.current;
14
+
15
+ // eslint-disable-next-line react-hooks/exhaustive-deps
16
+ return React.useMemo(() => dependenciesRef.current, [currentSignal]);
17
+ }
@@ -0,0 +1,44 @@
1
+ import React from 'react';
2
+
3
+ // import { marked } from 'marked';
4
+ // import { Renderer } from 'marked';
5
+ // import hljs from 'highlight.js';
6
+
7
+ export function useMarked(libs) {
8
+ const hljs = libs.highlightJs.default;
9
+ const { marked, Renderer } = libs.marked;
10
+
11
+ const renderer = React.useMemo(() => new Renderer(), [Renderer]);
12
+
13
+ renderer.paragraph = (text) => {
14
+ return text + '\n';
15
+ };
16
+ renderer.list = (text) => {
17
+ return `${text}\n\n`;
18
+ };
19
+ renderer.listitem = (text) => {
20
+ return `\n• ${text}`;
21
+ };
22
+ renderer.code = (code, language) => {
23
+ const validLanguage = hljs.getLanguage(language || '')
24
+ ? language
25
+ : 'plaintext';
26
+ const lang = validLanguage || 'plaintext';
27
+ const highlightedCode = hljs.highlight(lang, code).value;
28
+ return `<pre class="highlight bg-gray-700" style="padding: 5px; border-radius: 5px; overflow: auto; overflow-wrap: anywhere; white-space: pre-wrap; max-width: 100%; display: block; line-height: 1.2">
29
+ <code class="${language}" style="color: #d6e2ef; font-size: 12px; ">${highlightedCode}</code>
30
+ </pre>`;
31
+ };
32
+
33
+ marked.setOptions({ renderer });
34
+
35
+ const parser = React.useCallback(
36
+ async (msg) => {
37
+ const res = await marked.parse(msg);
38
+ return res;
39
+ },
40
+ [marked],
41
+ );
42
+
43
+ return { parser };
44
+ }
@@ -0,0 +1,119 @@
1
+ import React from 'react';
2
+ import loadable from '@loadable/component';
3
+
4
+ const Sentry = loadable.lib(
5
+ () => import(/* webpackChunkName: "s_entry-browser" */ '@sentry/browser'), // chunk name avoids ad blockers
6
+ );
7
+
8
+ async function fetchHalloumi(answer, sources, maxContextSegments) {
9
+ const halloumiResponse = await fetch('/_ha/generate', {
10
+ method: 'POST',
11
+ headers: {
12
+ 'Content-Type': 'application/json',
13
+ },
14
+ body: JSON.stringify({ answer, sources, maxContextSegments }),
15
+ });
16
+ return halloumiResponse;
17
+ }
18
+
19
+ const FAILURE_RATIONALE = 'Answer cannot be verified due to empty sources.';
20
+ // const TOOLARGE_RATIONALE = 'Verification failed: Too many sources provided.';
21
+ const TIMEOUT_RATIONALE =
22
+ 'Verification failed: Halloumi service is unreachable or timed out.';
23
+
24
+ const empty = (message, rationale, score = 0) => ({
25
+ claims: [
26
+ {
27
+ startOffset: 0,
28
+ endOffset: message.length,
29
+ score,
30
+ rationale,
31
+ },
32
+ ],
33
+ segments: {},
34
+ });
35
+
36
+ export function useQualityMarkers(
37
+ doQualityControl,
38
+ message,
39
+ sources,
40
+ maxContextSegments = 0,
41
+ ) {
42
+ const [halloumiResponse, setHalloumiResponse] = React.useState(null);
43
+ const [isLoading, setIsLoading] = React.useState(false);
44
+
45
+ const retryHalloumi = React.useCallback(() => {
46
+ setHalloumiResponse(null);
47
+ }, []);
48
+
49
+ React.useEffect(() => {
50
+ async function handler() {
51
+ const textSources = sources.map(({ halloumiContext }) => halloumiContext);
52
+ if (sources.length === 0) {
53
+ setHalloumiResponse(empty(message, FAILURE_RATIONALE));
54
+ return;
55
+ }
56
+
57
+ // // console.log('Halloumi sources:', sources.length, sources);
58
+ // if (sources.length > 40) {
59
+ // // eslint-disable-next-line no-console
60
+ // console.warn(
61
+ // `Warning: Too many sources (${sources.length}). Skipping quality control.`,
62
+ // );
63
+ //
64
+ // setHalloumiResponse(empty(message, TOOLARGE_RATIONALE));
65
+ // return;
66
+ // }
67
+
68
+ setIsLoading(true);
69
+
70
+ try {
71
+ const feedback = await fetchHalloumi(
72
+ message,
73
+ textSources,
74
+ maxContextSegments,
75
+ );
76
+ const body = await feedback.json();
77
+ // console.log({ message, sources, body });
78
+
79
+ if (body.error) {
80
+ setHalloumiResponse(empty(message, TIMEOUT_RATIONALE, null));
81
+ Sentry.load().then((mod) => mod.captureException(body.error));
82
+ } else {
83
+ setHalloumiResponse(body);
84
+ }
85
+ } catch {
86
+ setHalloumiResponse(empty(message, TIMEOUT_RATIONALE, null));
87
+ throw new Error(`Unknown error fetching halloumi response`);
88
+ } finally {
89
+ setIsLoading(false);
90
+ }
91
+ }
92
+
93
+ if (doQualityControl && !halloumiResponse) {
94
+ handler();
95
+ }
96
+ }, [
97
+ doQualityControl,
98
+ halloumiResponse,
99
+ message,
100
+ sources,
101
+ maxContextSegments,
102
+ ]);
103
+
104
+ if (halloumiResponse !== null) {
105
+ halloumiResponse.claims = halloumiResponse.claims.filter((claim) => {
106
+ const claim_text = message.substring(claim.startOffset, claim.endOffset);
107
+ const hasSpace = claim_text.trim().includes(' ');
108
+ const hasSpecialCharacters = /[^a-zA-Z0-9 ]/.test(claim_text);
109
+ const hasSmallScore = claim.score < 0.07;
110
+
111
+ return hasSpace || !hasSpecialCharacters || !hasSmallScore;
112
+ });
113
+ }
114
+ return {
115
+ markers: halloumiResponse,
116
+ isLoadingHalloumi: isLoading,
117
+ retryHalloumi,
118
+ };
119
+ }