@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,131 @@
1
+ import { useRef, useState, useEffect, useCallback } from 'react';
2
+
3
+ type ScrollonStreamProps = {
4
+ bottomRef?: React.RefObject<HTMLDivElement>;
5
+ isStreaming: boolean;
6
+ enabled?: boolean;
7
+ };
8
+
9
+ export function useScrollonStream({
10
+ bottomRef,
11
+ isStreaming,
12
+ ...props
13
+ }: ScrollonStreamProps) {
14
+ const [enabled, setEnabled] = useState(props.enabled ?? true);
15
+ const scrollIntervalRef = useRef<number | null>(null);
16
+ const stopStreamingTimeoutRef = useRef<number | null>(null);
17
+ const [isActive, setIsActive] = useState(isStreaming);
18
+
19
+ function clearScrollInterval() {
20
+ if (scrollIntervalRef.current) {
21
+ clearInterval(scrollIntervalRef.current);
22
+ scrollIntervalRef.current = null;
23
+ }
24
+ }
25
+
26
+ const disableScroll = useCallback((e: any) => {
27
+ const items = document.querySelectorAll('.tools-collapsed-header');
28
+ const expandToolsEl = items[items.length - 1];
29
+ if (expandToolsEl && e?.target && expandToolsEl.contains(e.target)) {
30
+ return;
31
+ }
32
+ clearScrollInterval();
33
+ setEnabled(false);
34
+ }, []);
35
+
36
+ // Track streaming state with grace period for brief interruptions
37
+ useEffect(() => {
38
+ if (stopStreamingTimeoutRef.current) {
39
+ clearTimeout(stopStreamingTimeoutRef.current);
40
+ }
41
+
42
+ if (isStreaming) {
43
+ setIsActive(true);
44
+ } else {
45
+ // Wait before considering streaming stopped
46
+ stopStreamingTimeoutRef.current = window.setTimeout(() => {
47
+ setIsActive(false);
48
+ }, 500);
49
+ }
50
+
51
+ return () => {
52
+ if (stopStreamingTimeoutRef.current) {
53
+ clearTimeout(stopStreamingTimeoutRef.current);
54
+ }
55
+ };
56
+ }, [isStreaming]);
57
+
58
+ // Listen for user input events that indicate scrolling intent
59
+ useEffect(() => {
60
+ if (!enabled) {
61
+ return;
62
+ }
63
+
64
+ const userEvents = ['wheel', 'touchstart', 'keydown', 'mousedown'];
65
+
66
+ userEvents.forEach((e) => {
67
+ window.addEventListener(e, disableScroll, { passive: true });
68
+ });
69
+
70
+ return () => {
71
+ userEvents.forEach((e) => {
72
+ window.removeEventListener(e, disableScroll);
73
+ });
74
+ };
75
+ }, [disableScroll, enabled]);
76
+
77
+ // Scroll to bottom when new content streams in
78
+ useEffect(() => {
79
+ function scrollToBottom() {
80
+ const bottomEl = bottomRef?.current;
81
+ if (!bottomEl) return;
82
+
83
+ const rect = bottomEl.getBoundingClientRect();
84
+ const offset = 24;
85
+
86
+ // Check if bottom element is already visible in viewport
87
+ const isVisible =
88
+ rect.top >= 0 &&
89
+ rect.bottom <= window.innerHeight &&
90
+ rect.left >= 0 &&
91
+ rect.right <= window.innerWidth;
92
+
93
+ // Don't scroll if element is already fully visible
94
+ if (isVisible) {
95
+ return;
96
+ }
97
+
98
+ const targetScrollY =
99
+ window.scrollY + rect.bottom - window.innerHeight + offset;
100
+
101
+ // Already at target position
102
+ if (Math.abs(targetScrollY - window.scrollY) < 1) {
103
+ return;
104
+ }
105
+
106
+ window.scrollTo({
107
+ top: targetScrollY,
108
+ behavior: 'smooth',
109
+ });
110
+ }
111
+
112
+ if (!enabled) {
113
+ return;
114
+ }
115
+
116
+ if (!isActive) {
117
+ // One final scroll when streaming stops
118
+ setTimeout(() => {
119
+ disableScroll(null);
120
+ scrollToBottom();
121
+ }, 100);
122
+ return;
123
+ }
124
+
125
+ scrollIntervalRef.current = window.setInterval(scrollToBottom, 100);
126
+
127
+ return () => {
128
+ clearScrollInterval();
129
+ };
130
+ }, [isActive, bottomRef, disableScroll, enabled]);
131
+ }
@@ -0,0 +1,80 @@
1
+ import type { Packet } from '../types/streamingModels';
2
+ import { useMemo, useState, useCallback, useEffect } from 'react';
3
+
4
+ interface ToolState {
5
+ isVisible: boolean;
6
+ isCompleted: boolean;
7
+ }
8
+
9
+ /**
10
+ * Simplified hook for tracking tool visibility and completion.
11
+ * All tools are shown immediately as they arrive (collapsed).
12
+ * No artificial delays - tools complete as soon as their rendering finishes.
13
+ */
14
+ export function useToolDisplayTiming(
15
+ toolGroups: { ind: number; packets: Packet[] }[],
16
+ isFinalMessageComing: boolean,
17
+ _isComplete: boolean,
18
+ ) {
19
+ const [toolStates, setToolStates] = useState<Map<number, ToolState>>(
20
+ () => new Map(),
21
+ );
22
+
23
+ // Make all tools visible immediately as they arrive
24
+ useEffect(() => {
25
+ if (toolGroups.length === 0) return;
26
+
27
+ setToolStates((prev) => {
28
+ const newStates = new Map(prev);
29
+ let hasChanges = false;
30
+
31
+ toolGroups.forEach((group) => {
32
+ if (!newStates.has(group.ind)) {
33
+ newStates.set(group.ind, {
34
+ isVisible: true,
35
+ isCompleted: false,
36
+ });
37
+ hasChanges = true;
38
+ }
39
+ });
40
+
41
+ return hasChanges ? newStates : prev;
42
+ });
43
+ }, [toolGroups]);
44
+
45
+ // Mark tool as completed immediately when called
46
+ const handleToolComplete = useCallback((toolInd: number) => {
47
+ setToolStates((prev) => {
48
+ const currentState = prev.get(toolInd);
49
+ if (!currentState || currentState.isCompleted) return prev;
50
+
51
+ const newStates = new Map(prev);
52
+ newStates.set(toolInd, { ...currentState, isCompleted: true });
53
+ return newStates;
54
+ });
55
+ }, []);
56
+
57
+ // All tools are visible immediately
58
+ const visibleTools = useMemo(
59
+ () => new Set(toolGroups.map((group) => group.ind)),
60
+ [toolGroups],
61
+ );
62
+
63
+ // All tools are displayed when all are completed and final message is coming
64
+ const allToolsDisplayed = useMemo(() => {
65
+ if (toolGroups.length === 0) return true;
66
+
67
+ const allCompleted = toolGroups.every((group) => {
68
+ const state = toolStates.get(group.ind);
69
+ return state?.isCompleted;
70
+ });
71
+
72
+ return allCompleted && isFinalMessageComing;
73
+ }, [toolGroups, toolStates, isFinalMessageComing]);
74
+
75
+ return {
76
+ visibleTools,
77
+ handleToolComplete,
78
+ allToolsDisplayed,
79
+ };
80
+ }
@@ -0,0 +1,32 @@
1
+ import codeSVG from '@plone/volto/icons/code.svg';
2
+ import ChatBlockView from './ChatBlockView';
3
+ import ChatBlockEdit from './ChatBlockEdit';
4
+ import { ChatBlockSchema } from './schema';
5
+
6
+ export default function installChatBlock(config) {
7
+ config.blocks.blocksConfig.eeaChatbot = {
8
+ id: 'eea_chatbot',
9
+ title: 'AI Chatbot',
10
+ icon: codeSVG,
11
+ group: 'common',
12
+ view: ChatBlockView,
13
+ edit: ChatBlockEdit,
14
+ restricted: function ({ user }) {
15
+ if (user?.roles) {
16
+ return !user.roles.find((role) => role === 'Manager');
17
+ }
18
+ // backward compatibility for older Volto versions
19
+ return false;
20
+ },
21
+ mostUsed: false,
22
+ blockHasOwnFocusManagement: false,
23
+ sidebarTab: 1,
24
+ schema: ChatBlockSchema,
25
+ security: {
26
+ addPermission: [],
27
+ view: [],
28
+ },
29
+ variations: [],
30
+ };
31
+ return config;
32
+ }
@@ -0,0 +1,235 @@
1
+ import type { Packet } from '../types/streamingModels';
2
+ import type { Message } from '../types/interfaces';
3
+ import React, { useState, useEffect, useMemo } from 'react';
4
+ import cx from 'classnames';
5
+ import { PacketType } from '../types/streamingModels';
6
+ import { RendererComponent } from './RendererComponent';
7
+ import { useToolDisplayTiming } from '../hooks/useToolDisplayTiming';
8
+ import SVGIcon from '../components/Icon';
9
+ import DoneIcon from '../../icons/done.svg';
10
+ import ChevronIcon from '../../icons/chevron.svg';
11
+
12
+ interface MultiToolRendererProps {
13
+ toolGroups: { ind: number; packets: Packet[] }[];
14
+ showTools?: PacketType[];
15
+ message: Message;
16
+ libs: any;
17
+ onAllToolsDisplayed?: () => void;
18
+ }
19
+
20
+ export function MultiToolRenderer({
21
+ toolGroups,
22
+ showTools = [PacketType.SEARCH_TOOL_START],
23
+ message,
24
+ libs,
25
+ onAllToolsDisplayed,
26
+ }: MultiToolRendererProps) {
27
+ const [isExpanded, setIsExpanded] = useState(false);
28
+ const { isFinalMessageComing = false, isComplete = false } = message;
29
+
30
+ // Filter tool groups based on allowed tool types
31
+ const filteredToolGroups = useMemo(
32
+ () =>
33
+ toolGroups.filter(
34
+ (group) =>
35
+ group.packets?.some(
36
+ (packet) => showTools?.includes(packet.obj.type as PacketType),
37
+ ),
38
+ ),
39
+ [toolGroups, showTools],
40
+ );
41
+
42
+ // Manage tool display timing
43
+ const { allToolsDisplayed, handleToolComplete } = useToolDisplayTiming(
44
+ filteredToolGroups,
45
+ isFinalMessageComing,
46
+ isComplete,
47
+ );
48
+
49
+ // Notify parent when all tools are displayed
50
+ useEffect(() => {
51
+ if (allToolsDisplayed && onAllToolsDisplayed) {
52
+ onAllToolsDisplayed();
53
+ setIsExpanded(false);
54
+ }
55
+ }, [allToolsDisplayed, onAllToolsDisplayed]);
56
+
57
+ const toggleExpanded = () => setIsExpanded(!isExpanded);
58
+
59
+ const handleKeyDown = (e: React.KeyboardEvent) => {
60
+ if (e.key === 'Enter' || e.key === ' ') {
61
+ e.preventDefault();
62
+ toggleExpanded();
63
+ }
64
+ };
65
+
66
+ if (filteredToolGroups.length === 0) return null;
67
+
68
+ const isStreaming = !allToolsDisplayed;
69
+
70
+ const count = filteredToolGroups.length;
71
+
72
+ const ariaLabel = `${count} ${isStreaming ? 'processing' : 'completed'} ${
73
+ count === 1 ? 'step' : 'steps'
74
+ }, ${isExpanded ? 'expanded' : 'collapsed'}`;
75
+
76
+ return (
77
+ <div
78
+ className={cx('multi-tool-renderer', {
79
+ streaming: isStreaming,
80
+ complete: !isStreaming,
81
+ })}
82
+ >
83
+ {/* Header */}
84
+ <div className={cx({ 'tools-container collapsed-view': isStreaming })}>
85
+ <div
86
+ className={cx({
87
+ 'tools-collapsed-header': isStreaming,
88
+ 'tools-summary-header': !isStreaming,
89
+ })}
90
+ onClick={toggleExpanded}
91
+ role="button"
92
+ tabIndex={0}
93
+ aria-expanded={isExpanded}
94
+ aria-label={ariaLabel}
95
+ onKeyDown={handleKeyDown}
96
+ >
97
+ <div className="tools-count">
98
+ <span className="tools-count-value">
99
+ {filteredToolGroups.length}
100
+ </span>
101
+ <span className="tools-count-label">
102
+ {filteredToolGroups.length === 1 ? 'step' : 'steps'}
103
+ </span>
104
+ </div>
105
+ <span className={cx('expand-chevron', { expanded: isExpanded })}>
106
+ <SVGIcon name={ChevronIcon} size={24} />
107
+ </span>
108
+ </div>
109
+
110
+ {/* Tools List */}
111
+ <div
112
+ className={cx({
113
+ 'tools-collapsed-list': isStreaming,
114
+ 'tools-expanded-content': !isStreaming,
115
+ expanded: isExpanded && isStreaming,
116
+ visible: isExpanded && !isStreaming,
117
+ })}
118
+ >
119
+ <div className={cx({ 'tools-list': isStreaming })}>
120
+ <div>
121
+ {filteredToolGroups.map((toolGroup, index) => {
122
+ const isLastItem = index === filteredToolGroups.length - 1;
123
+
124
+ return (
125
+ <div
126
+ key={toolGroup.ind}
127
+ className={cx({ 'tool-collaps ed-wrapper': isStreaming })}
128
+ >
129
+ <RendererComponent
130
+ packets={toolGroup.packets}
131
+ message={message}
132
+ libs={libs}
133
+ onComplete={() => {
134
+ if (toolGroup.ind !== undefined) {
135
+ handleToolComplete(toolGroup.ind);
136
+ }
137
+ }}
138
+ stopPacketSeen={isComplete}
139
+ animate={false}
140
+ >
141
+ {({ icon, content, status, expandedText }) => {
142
+ const finalIcon = icon ? (
143
+ React.createElement(icon, { size: 14 })
144
+ ) : (
145
+ <span
146
+ className={cx({
147
+ 'tool-icon-dot': isStreaming,
148
+ 'tool-icon-default': !isStreaming,
149
+ })}
150
+ />
151
+ );
152
+
153
+ // Streaming: collapsed view (status only)
154
+ if (isStreaming) {
155
+ return (
156
+ <div
157
+ className={cx('tool-item-collapsed', {
158
+ active: isLastItem,
159
+ completed: !isLastItem,
160
+ })}
161
+ >
162
+ <div className="tool-collapsed-icon">
163
+ {finalIcon}
164
+ </div>
165
+ <span className="tool-collapsed-status">
166
+ {status}
167
+ </span>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ // Complete: expanded view (full content)
173
+ return (
174
+ <div className="tool-item-expanded">
175
+ <div className="tool-connector-line" />
176
+
177
+ <div className="tool-item-row">
178
+ <div className="tool-icon-wrapper">
179
+ <div className="tool-icon-circle">
180
+ {finalIcon}
181
+ </div>
182
+ </div>
183
+
184
+ <div
185
+ className={cx('tool-content', {
186
+ 'with-padding': !isLastItem,
187
+ })}
188
+ >
189
+ {status && !expandedText && (
190
+ <div className="tool-status-row">
191
+ <div className="tool-status">{status}</div>
192
+ </div>
193
+ )}
194
+
195
+ <div
196
+ className={cx('tool-text', {
197
+ expanded: expandedText,
198
+ })}
199
+ >
200
+ {expandedText || content}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+ );
206
+ }}
207
+ </RendererComponent>
208
+ </div>
209
+ );
210
+ })}
211
+
212
+ {/* Done node - only in complete state */}
213
+ {allToolsDisplayed && (
214
+ <div className="tool-done-node">
215
+ <div className="tool-done-row">
216
+ <div className="tool-icon-wrapper">
217
+ <div className="tool-icon-circle">
218
+ <span className="check-icon">
219
+ <SVGIcon name={DoneIcon} size={14} />
220
+ </span>
221
+ </div>
222
+ </div>
223
+ <div className="tool-done-content">
224
+ <div className="tool-done-text">Done</div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ )}
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ </div>
234
+ );
235
+ }
@@ -0,0 +1,115 @@
1
+ import type { Packet } from '../types/streamingModels';
2
+ import type { RendererResult, Message } from '../types/interfaces';
3
+ import { PacketType } from '../types/streamingModels';
4
+ import {
5
+ MessageTextRenderer,
6
+ SearchToolRenderer,
7
+ ImageToolRenderer,
8
+ ReasoningRenderer,
9
+ CustomToolRenderer,
10
+ FetchToolRenderer,
11
+ } from './renderers';
12
+
13
+ interface GroupedPackets {
14
+ packets: Packet[];
15
+ }
16
+
17
+ function isChatPacket(packet: Packet): boolean {
18
+ return (
19
+ packet.obj.type === PacketType.MESSAGE_START ||
20
+ packet.obj.type === PacketType.MESSAGE_DELTA ||
21
+ packet.obj.type === PacketType.MESSAGE_END
22
+ );
23
+ }
24
+
25
+ function isSearchToolPacket(packet: Packet): boolean {
26
+ return packet.obj.type === PacketType.SEARCH_TOOL_START;
27
+ }
28
+
29
+ function isImageToolPacket(packet: Packet): boolean {
30
+ return packet.obj.type === PacketType.IMAGE_GENERATION_TOOL_START;
31
+ }
32
+
33
+ function isCustomToolPacket(packet: Packet): boolean {
34
+ return packet.obj.type === PacketType.CUSTOM_TOOL_START;
35
+ }
36
+
37
+ function isFetchToolPacket(packet: Packet): boolean {
38
+ return packet.obj.type === PacketType.FETCH_TOOL_START;
39
+ }
40
+
41
+ function isReasoningPacket(packet: Packet): boolean {
42
+ return (
43
+ packet.obj.type === PacketType.REASONING_START ||
44
+ packet.obj.type === PacketType.REASONING_DELTA
45
+ );
46
+ }
47
+
48
+ export function findRenderer(groupedPackets: GroupedPackets): any | null {
49
+ if (groupedPackets.packets.some((packet) => isChatPacket(packet))) {
50
+ return MessageTextRenderer;
51
+ }
52
+ if (groupedPackets.packets.some((packet) => isSearchToolPacket(packet))) {
53
+ return SearchToolRenderer;
54
+ }
55
+ if (groupedPackets.packets.some((packet) => isImageToolPacket(packet))) {
56
+ return ImageToolRenderer;
57
+ }
58
+ if (groupedPackets.packets.some((packet) => isCustomToolPacket(packet))) {
59
+ return CustomToolRenderer;
60
+ }
61
+ if (groupedPackets.packets.some((packet) => isFetchToolPacket(packet))) {
62
+ return FetchToolRenderer;
63
+ }
64
+ if (groupedPackets.packets.some((packet) => isReasoningPacket(packet))) {
65
+ return ReasoningRenderer;
66
+ }
67
+ return null;
68
+ }
69
+
70
+ // React component wrapper that directly uses renderer components
71
+ export function RendererComponent({
72
+ packets,
73
+ onComplete,
74
+ animate,
75
+ stopPacketSeen,
76
+ children,
77
+ message,
78
+ libs,
79
+ markers,
80
+ stableContextSources,
81
+ addQualityMarkersPlugin,
82
+ }: {
83
+ packets: Packet[];
84
+ onComplete: () => void;
85
+ animate: boolean;
86
+ stopPacketSeen: boolean;
87
+ children: (result: RendererResult) => JSX.Element;
88
+ libs: any;
89
+ message?: Message;
90
+ markers?: any;
91
+ stableContextSources?: any;
92
+ addQualityMarkersPlugin?: any;
93
+ }) {
94
+ const RendererFn = findRenderer({ packets });
95
+
96
+ if (!RendererFn) {
97
+ return children({ icon: null, status: null, content: <></> });
98
+ }
99
+
100
+ return (
101
+ <RendererFn
102
+ packets={packets as any}
103
+ onComplete={onComplete}
104
+ animate={animate}
105
+ stopPacketSeen={stopPacketSeen}
106
+ message={message}
107
+ libs={libs}
108
+ markers={markers}
109
+ stableContextSources={stableContextSources}
110
+ addQualityMarkersPlugin={addQualityMarkersPlugin}
111
+ >
112
+ {children}
113
+ </RendererFn>
114
+ );
115
+ }
@@ -0,0 +1,4 @@
1
+ export * from './renderers';
2
+
3
+ export { MultiToolRenderer } from './MultiToolRenderer';
4
+ export { RendererComponent } from './RendererComponent';
@@ -0,0 +1,63 @@
1
+ import type {
2
+ CustomToolPacket,
3
+ CustomToolStart,
4
+ CustomToolDelta,
5
+ } from '../../types/streamingModels';
6
+ import type { MessageRenderer } from '../../types/interfaces';
7
+ import { useEffect } from 'react';
8
+ import { PacketType } from '../../types/streamingModels';
9
+
10
+ export const CustomToolRenderer: MessageRenderer<CustomToolPacket> = ({
11
+ packets,
12
+ onComplete,
13
+ children,
14
+ }) => {
15
+ const toolStart = packets.find(
16
+ (packet) => packet.obj.type === PacketType.CUSTOM_TOOL_START,
17
+ )?.obj as CustomToolStart | undefined;
18
+
19
+ const toolDeltas = packets
20
+ .filter((packet) => packet.obj.type === PacketType.CUSTOM_TOOL_DELTA)
21
+ .map((packet) => packet.obj as CustomToolDelta);
22
+
23
+ const isComplete = packets.some(
24
+ (packet) => packet.obj.type === PacketType.SECTION_END,
25
+ );
26
+
27
+ useEffect(() => {
28
+ if (isComplete) {
29
+ onComplete();
30
+ }
31
+ }, [isComplete, onComplete]);
32
+
33
+ const toolName = toolStart?.tool_name || 'Custom Tool';
34
+
35
+ const content = (
36
+ <div className="custom-tool-renderer">
37
+ <div className="tool-header">
38
+ <span className="tool-icon">🔧</span>
39
+ <strong>{toolName}</strong>
40
+ </div>
41
+ <div className="tool-results">
42
+ {toolDeltas.map((delta, i) => (
43
+ <div key={i} className="tool-result-item">
44
+ {delta.response_type && (
45
+ <div className="response-type">{delta.response_type}</div>
46
+ )}
47
+ {delta.data && (
48
+ <pre className="tool-data">
49
+ {JSON.stringify(delta.data, null, 2)}
50
+ </pre>
51
+ )}
52
+ </div>
53
+ ))}
54
+ </div>
55
+ </div>
56
+ );
57
+
58
+ return children({
59
+ icon: null,
60
+ status: isComplete ? 'Tool complete' : 'Running tool...',
61
+ content,
62
+ });
63
+ };