@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,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
|
+
};
|