@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,59 @@
1
+ import type {
2
+ FetchToolPacket,
3
+ FetchToolStart,
4
+ } from '../../types/streamingModels';
5
+ import type { MessageRenderer } from '../../types/interfaces';
6
+ import { useEffect } from 'react';
7
+ import { PacketType } from '../../types/streamingModels';
8
+
9
+ export const FetchToolRenderer: MessageRenderer<FetchToolPacket> = ({
10
+ packets,
11
+ onComplete,
12
+ children,
13
+ }) => {
14
+ const fetchStart = packets.find(
15
+ (packet) => packet.obj.type === PacketType.FETCH_TOOL_START,
16
+ )?.obj as FetchToolStart | undefined;
17
+
18
+ const queries = fetchStart?.queries || [];
19
+ const documents = fetchStart?.documents || [];
20
+
21
+ const isComplete = packets.some(
22
+ (packet) => packet.obj.type === PacketType.SECTION_END,
23
+ );
24
+
25
+ useEffect(() => {
26
+ if (isComplete) {
27
+ onComplete();
28
+ }
29
+ }, [isComplete, onComplete]);
30
+
31
+ const content = (
32
+ <div className="fetch-tool-renderer">
33
+ <div className="fetch-header">
34
+ <span className="fetch-icon">📥</span>
35
+ <strong>Fetching Documents</strong>
36
+ </div>
37
+ {queries.length > 0 && (
38
+ <div className="fetch-queries">
39
+ {queries.map((query, i) => (
40
+ <div key={i} className="query-item">
41
+ {query}
42
+ </div>
43
+ ))}
44
+ </div>
45
+ )}
46
+ {documents.length > 0 && (
47
+ <div className="fetch-results">
48
+ <div>Fetched {documents.length} documents</div>
49
+ </div>
50
+ )}
51
+ </div>
52
+ );
53
+
54
+ return children({
55
+ icon: null,
56
+ status: isComplete ? 'Fetch complete' : 'Fetching...',
57
+ content,
58
+ });
59
+ };
@@ -0,0 +1,62 @@
1
+ import { useEffect } from 'react';
2
+ import type {
3
+ ImageGenerationToolPacket,
4
+ ImageGenerationToolDelta,
5
+ GeneratedImage,
6
+ } from '../../types/streamingModels';
7
+ import type { MessageRenderer } from '../../types/interfaces';
8
+ import { PacketType } from '../../types/streamingModels';
9
+
10
+ export const ImageToolRenderer: MessageRenderer<ImageGenerationToolPacket> = ({
11
+ packets,
12
+ onComplete,
13
+ children,
14
+ }) => {
15
+ const imageDeltas = packets
16
+ .filter(
17
+ (packet) => packet.obj.type === PacketType.IMAGE_GENERATION_TOOL_DELTA,
18
+ )
19
+ .map((packet) => packet.obj as ImageGenerationToolDelta);
20
+
21
+ const images: GeneratedImage[] = imageDeltas.flatMap(
22
+ (delta) => delta.images || [],
23
+ );
24
+
25
+ const isComplete = packets.some(
26
+ (packet) => packet.obj.type === PacketType.SECTION_END,
27
+ );
28
+
29
+ useEffect(() => {
30
+ if (isComplete) {
31
+ onComplete();
32
+ }
33
+ }, [isComplete, onComplete]);
34
+
35
+ const content = (
36
+ <div className="image-tool-renderer">
37
+ <div className="images-header">
38
+ <strong>Generated Images</strong>
39
+ </div>
40
+ <div className="images-grid">
41
+ {images.map((image, i) => (
42
+ <div key={image.file_id} className="image-item">
43
+ <img
44
+ src={image.url}
45
+ alt={image.revised_prompt}
46
+ className={`image-shape-${image.shape || 'square'}`}
47
+ />
48
+ {image.revised_prompt && (
49
+ <div className="image-prompt">{image.revised_prompt}</div>
50
+ )}
51
+ </div>
52
+ ))}
53
+ </div>
54
+ </div>
55
+ );
56
+
57
+ return children({
58
+ icon: null,
59
+ status: isComplete ? 'Images generated' : 'Generating images...',
60
+ content,
61
+ });
62
+ };
@@ -0,0 +1,172 @@
1
+ import type { ChatPacket } from '../../types/streamingModels';
2
+ import type { MessageRenderer } from '../../types/interfaces';
3
+ import { useEffect, useMemo, useState } from 'react';
4
+ import loadable from '@loadable/component';
5
+ import { components } from '../../components/markdown';
6
+ import { isFinalAnswerComplete } from '../../services/packetUtils';
7
+ import { PacketType } from '../../types/streamingModels';
8
+ import { BlinkingDot } from '../../components/BlinkingDot';
9
+
10
+ const Markdown: any = loadable(() => import('react-markdown'));
11
+
12
+ // Control the rate of packet streaming (packets per second)
13
+ const PACKET_DELAY_MS = 10;
14
+ // Number of packets to show per animation tick when catching up
15
+ const PACKETS_PER_TICK = 5;
16
+ // Adaptive animation constants for fast streams
17
+ const MIN_REVEAL_DURATION_MS = 400; // Minimum typing animation duration
18
+ const PACKET_VALUE_MS = 1.5; // Additional time per packet (scales with length)
19
+ const MAX_REVEAL_DURATION_MS = 2000; // Cap to prevent slow reveals
20
+ const CATCH_UP_THRESHOLD = 20; // Threshold for catch-up mode
21
+
22
+ export const MessageTextRenderer: MessageRenderer<ChatPacket> = ({
23
+ packets,
24
+ onComplete,
25
+ animate,
26
+ stopPacketSeen,
27
+ children,
28
+ message,
29
+ libs,
30
+ markers,
31
+ stableContextSources,
32
+ addQualityMarkersPlugin,
33
+ }) => {
34
+ const { remarkGfm } = libs;
35
+
36
+ // Check if stream is finished
37
+ const isStreamFinished = isFinalAnswerComplete(packets);
38
+
39
+ // If we're animating and the final answer is already complete, show more packets initially
40
+ const initialPacketCount = animate
41
+ ? packets.length > 0
42
+ ? 1 // Start with 1 packet
43
+ : 0
44
+ : -1; // Show all if not animating
45
+
46
+ const [displayedPacketCount, setDisplayedPacketCount] =
47
+ useState(initialPacketCount);
48
+
49
+ // Animation effect - gradually increase displayed packets at controlled rate
50
+ // Adaptive animation: ensures visible typing effect even for fast streams
51
+ useEffect(() => {
52
+ if (!animate) {
53
+ setDisplayedPacketCount(-1); // Show all packets
54
+ return;
55
+ }
56
+
57
+ if (displayedPacketCount >= 0 && displayedPacketCount < packets.length) {
58
+ // CASE 1: Stream finished - apply adaptive animation
59
+ if (isStreamFinished) {
60
+ const remainingPackets = packets.length - displayedPacketCount;
61
+
62
+ // Calculate adaptive reveal velocity
63
+ const targetDuration =
64
+ MIN_REVEAL_DURATION_MS + remainingPackets * PACKET_VALUE_MS;
65
+ const cappedDuration = Math.min(targetDuration, MAX_REVEAL_DURATION_MS);
66
+ const ticksNeeded = Math.max(1, cappedDuration / PACKET_DELAY_MS);
67
+ const packetsPerTick = Math.ceil(remainingPackets / ticksNeeded);
68
+
69
+ const timer = setTimeout(() => {
70
+ setDisplayedPacketCount((prev) => {
71
+ return Math.min(prev + packetsPerTick, packets.length);
72
+ });
73
+ }, PACKET_DELAY_MS);
74
+
75
+ return () => clearTimeout(timer);
76
+ }
77
+
78
+ // CASE 2: Normal streaming - existing catch-up logic
79
+ const timer = setTimeout(() => {
80
+ setDisplayedPacketCount((prev) => {
81
+ const remaining = packets.length - prev;
82
+ // If we're far behind, catch up faster
83
+ const increment =
84
+ remaining > CATCH_UP_THRESHOLD ? PACKETS_PER_TICK : 1;
85
+ return Math.min(prev + increment, packets.length);
86
+ });
87
+ }, PACKET_DELAY_MS);
88
+
89
+ return () => clearTimeout(timer);
90
+ }
91
+ }, [animate, displayedPacketCount, packets.length, isStreamFinished]);
92
+
93
+ // Reset displayed count when packet array changes significantly (e.g., new message)
94
+ useEffect(() => {
95
+ if (animate && packets.length < displayedPacketCount) {
96
+ const resetCount = isStreamFinished
97
+ ? packets.length // Show all if stream is finished
98
+ : packets.length > 0
99
+ ? 1
100
+ : 0;
101
+ setDisplayedPacketCount(resetCount);
102
+ }
103
+ }, [animate, packets.length, displayedPacketCount, isStreamFinished]);
104
+
105
+ // Only mark as complete when all packets are received AND displayed
106
+ useEffect(() => {
107
+ if (isStreamFinished) {
108
+ // If animating, wait until all packets are displayed
109
+ if (
110
+ animate &&
111
+ displayedPacketCount >= 0 &&
112
+ displayedPacketCount < packets.length
113
+ ) {
114
+ return;
115
+ }
116
+ onComplete();
117
+ }
118
+ }, [
119
+ packets.length,
120
+ onComplete,
121
+ animate,
122
+ displayedPacketCount,
123
+ isStreamFinished,
124
+ ]);
125
+
126
+ // Get content based on displayed packet count
127
+ const content = useMemo(() => {
128
+ if (!animate || displayedPacketCount === -1) {
129
+ return message.message; // Show all content
130
+ }
131
+
132
+ // Only show content from packets up to displayedPacketCount
133
+ return packets
134
+ .slice(0, displayedPacketCount)
135
+ .map((packet) => {
136
+ if (
137
+ packet.obj.type === PacketType.MESSAGE_DELTA ||
138
+ packet.obj.type === PacketType.MESSAGE_START
139
+ ) {
140
+ return packet.obj.content;
141
+ }
142
+ return '';
143
+ })
144
+ .join('');
145
+ }, [animate, displayedPacketCount, message.message, packets]);
146
+
147
+ // Add blinking cursor when streaming
148
+ const displayContent = stopPacketSeen ? content : content + ' ▊';
149
+
150
+ const renderedContent = (
151
+ <div className="message-text-content">
152
+ <Markdown
153
+ components={components(message, markers, stableContextSources)}
154
+ remarkPlugins={remarkGfm ? [remarkGfm.default] : []}
155
+ rehypePlugins={addQualityMarkersPlugin ? [addQualityMarkersPlugin] : []}
156
+ >
157
+ {displayContent}
158
+ </Markdown>
159
+ </div>
160
+ );
161
+
162
+ return children({
163
+ icon: null,
164
+ status: null,
165
+ content:
166
+ content.length > 0 || packets.length > 0 ? (
167
+ renderedContent
168
+ ) : (
169
+ <BlinkingDot addMargin />
170
+ ),
171
+ });
172
+ };
@@ -0,0 +1,122 @@
1
+ import type {
2
+ ReasoningPacket,
3
+ ReasoningDelta,
4
+ } from '../../types/streamingModels';
5
+ import type { MessageRenderer } from '../../types/interfaces';
6
+ import { useEffect, useState, useRef, useMemo } from 'react';
7
+ import loadable from '@loadable/component';
8
+ import { PacketType } from '../../types/streamingModels';
9
+ import { components } from '../../components/markdown';
10
+ import { addCitations } from '../../utils/citations';
11
+
12
+ const Markdown: any = loadable(() => import('react-markdown'));
13
+
14
+ const THINKING_MIN_DURATION_MS = 500; // 0.5 second minimum for "Thinking" state
15
+ const THINKING_STATUS = 'Thinking';
16
+
17
+ function constructCurrentReasoningState(packets: ReasoningPacket[]) {
18
+ const hasStart = packets.some(
19
+ (p) => p.obj.type === PacketType.REASONING_START,
20
+ );
21
+ const hasEnd = packets.some(
22
+ (p) =>
23
+ p.obj.type === PacketType.SECTION_END ||
24
+ // Support either convention for reasoning completion
25
+ (p.obj as any).type === PacketType.REASONING_END,
26
+ );
27
+ const deltas = packets
28
+ .filter((p) => p.obj.type === PacketType.REASONING_DELTA)
29
+ .map((p) => p.obj as ReasoningDelta);
30
+
31
+ const content = deltas.map((d) => d.reasoning).join('');
32
+
33
+ return {
34
+ hasStart,
35
+ hasEnd,
36
+ content,
37
+ };
38
+ }
39
+
40
+ export const ReasoningRenderer: MessageRenderer<ReasoningPacket> = ({
41
+ packets,
42
+ onComplete,
43
+ animate,
44
+ children,
45
+ message,
46
+ libs,
47
+ }) => {
48
+ const { remarkGfm } = libs;
49
+
50
+ const { hasStart, hasEnd, content } = useMemo(() => {
51
+ return constructCurrentReasoningState(packets);
52
+ }, [packets]);
53
+
54
+ // Track reasoning timing for minimum display duration
55
+ const [reasoningStartTime, setReasoningStartTime] = useState<number | null>(
56
+ null,
57
+ );
58
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
59
+ const completionHandledRef = useRef(false);
60
+
61
+ // Track when reasoning starts
62
+ useEffect(() => {
63
+ if ((hasStart || hasEnd) && reasoningStartTime === null) {
64
+ setReasoningStartTime(Date.now());
65
+ }
66
+ }, [hasStart, hasEnd, reasoningStartTime]);
67
+
68
+ // Handle reasoning completion with minimum duration
69
+ useEffect(() => {
70
+ if (
71
+ hasEnd &&
72
+ reasoningStartTime !== null &&
73
+ !completionHandledRef.current
74
+ ) {
75
+ completionHandledRef.current = true;
76
+ const elapsedTime = Date.now() - reasoningStartTime;
77
+ const minimumThinkingDuration = animate ? THINKING_MIN_DURATION_MS : 0;
78
+
79
+ if (elapsedTime >= minimumThinkingDuration) {
80
+ // Enough time has passed, complete immediately
81
+ onComplete();
82
+ } else {
83
+ // Not enough time has passed, delay completion
84
+ const remainingTime = minimumThinkingDuration - elapsedTime;
85
+ timeoutRef.current = setTimeout(() => {
86
+ onComplete();
87
+ }, remainingTime);
88
+ }
89
+ }
90
+ }, [hasEnd, reasoningStartTime, animate, onComplete]);
91
+
92
+ // Cleanup timeout on unmount
93
+ useEffect(() => {
94
+ return () => {
95
+ if (timeoutRef.current) {
96
+ clearTimeout(timeoutRef.current);
97
+ }
98
+ };
99
+ }, []);
100
+
101
+ const renderedContent = (
102
+ <div className="reasoning-content">
103
+ <Markdown
104
+ remarkPlugins={remarkGfm ? [remarkGfm.default] : []}
105
+ components={components(message)}
106
+ >
107
+ {addCitations(content, message)}
108
+ </Markdown>
109
+ </div>
110
+ );
111
+
112
+ if (!hasStart && !hasEnd && content.length === 0) {
113
+ return children({ icon: null, status: null, content: <></> });
114
+ }
115
+
116
+ return children({
117
+ icon: null,
118
+ status: THINKING_STATUS,
119
+ content: renderedContent,
120
+ expandedText: renderedContent,
121
+ });
122
+ };