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