@agentscope-ai/chat 1.1.71-beta.1782728583669 → 1.1.71
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/components/AgentScopeRuntimeWebUI/core/Chat/Input/index.tsx +16 -75
- package/components/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.tsx +150 -82
- package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/helpers.ts +224 -0
- package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/index.tsx +673 -0
- package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/types.ts +27 -0
- package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/index.tsx +114 -26
- package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.tsx +122 -265
- package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.tsx +6 -32
- package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.tsx +9 -17
- package/components/AgentScopeRuntimeWebUI/core/Chat/index.tsx +3 -42
- package/components/AgentScopeRuntimeWebUI/core/Chat/styles.tsx +322 -254
- package/components/AgentScopeRuntimeWebUI/core/ChatAnywhere/index.tsx +1 -1
- package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.tsx +8 -34
- package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereInputContext.tsx +11 -4
- package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereSessionsContext.tsx +15 -82
- package/components/AgentScopeRuntimeWebUI/core/Layout/styles.tsx +3 -3
- package/components/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.ts +66 -68
- package/components/AgentScopeRuntimeWebUI/core/types/ISessions.tsx +1 -5
- package/components/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.ts +5 -0
- package/components/AgentScopeRuntimeWebUI/starter/index.tsx +20 -99
- package/components/Sender/index.tsx +157 -26
- package/components/Sender/style/index.ts +13 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.d.ts +0 -8
- package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.js +24 -104
- package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.d.ts +6 -4
- package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.js +153 -101
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/helpers.d.ts +16 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/helpers.js +200 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/index.d.ts +2 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/index.js +743 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/types.d.ts +24 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/types.js +1 -0
- package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/index.js +154 -28
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.d.ts +1 -15
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.js +147 -259
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.d.ts +1 -1
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.js +65 -98
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.d.ts +3 -3
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.js +12 -33
- package/lib/AgentScopeRuntimeWebUI/core/Chat/index.js +2 -33
- package/lib/AgentScopeRuntimeWebUI/core/Chat/styles.js +52 -44
- package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.d.ts +1 -23
- package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.js +8 -32
- package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereInputContext.js +8 -4
- package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereSessionsContext.js +21 -110
- package/lib/AgentScopeRuntimeWebUI/core/Layout/styles.js +1 -1
- package/lib/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.d.ts +60 -79
- package/lib/AgentScopeRuntimeWebUI/core/types/ISessions.d.ts +0 -4
- package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.d.ts +5 -0
- package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.js +5 -0
- package/lib/AgentScopeRuntimeWebUI/starter/index.js +26 -144
- package/lib/Sender/index.d.ts +29 -5
- package/lib/Sender/index.js +116 -21
- package/lib/Sender/style/index.js +11 -1
- package/package.json +1 -2
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.tsx +0 -289
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.scenarios.zh-CN.md +0 -51
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.test.ts +0 -758
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.ts +0 -421
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.ts +0 -375
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.ts +0 -102
- package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.ts +0 -87
- package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.tsx +0 -1084
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.d.ts +0 -15
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.js +0 -275
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.d.ts +0 -10
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.js +0 -586
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.d.ts +0 -90
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.js +0 -242
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.d.ts +0 -17
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.js +0 -39
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.d.ts +0 -5
- package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.js +0 -108
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.d.ts +0 -59
- package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.js +0 -935
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { Popover, Tooltip } from '@agentscope-ai/design';
|
|
2
|
+
import cls from 'classnames';
|
|
3
|
+
import { ChevronDown, ChevronUp, List as ListIcon, LocateFixed } from 'lucide-react';
|
|
4
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
5
|
+
import {
|
|
6
|
+
areAnchorPositionsEqual,
|
|
7
|
+
clamp,
|
|
8
|
+
getActiveAnchorId,
|
|
9
|
+
getAnchorTimeText,
|
|
10
|
+
getMessageElementInScrollContainer,
|
|
11
|
+
getMessageElementMapInScrollContainer,
|
|
12
|
+
getScrollVisibleTopOffset,
|
|
13
|
+
getTargetTopOffset,
|
|
14
|
+
getUserMessageAnchorBadgeText,
|
|
15
|
+
getUserMessageAnchor,
|
|
16
|
+
getUserMessageAnchorMinGap,
|
|
17
|
+
getUserMessageAnchorMinCount,
|
|
18
|
+
scrollTargetIntoContainerTop,
|
|
19
|
+
} from './helpers';
|
|
20
|
+
import type { UserMessageAnchor, UserMessageAnchorsProps } from './types';
|
|
21
|
+
|
|
22
|
+
type UserMessageAnchorGroup = {
|
|
23
|
+
active: boolean;
|
|
24
|
+
anchors: UserMessageAnchor[];
|
|
25
|
+
id: string;
|
|
26
|
+
targetAnchor: UserMessageAnchor;
|
|
27
|
+
top: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ANCHOR_CONTENT_GAP = 24;
|
|
31
|
+
const ANCHOR_TRACK_WIDTH = 32;
|
|
32
|
+
const NAVIGATOR_TRACK_WIDTH = 44;
|
|
33
|
+
const TARGET_ACTIVE_DURATION = 1200;
|
|
34
|
+
const TARGET_TOP_TOLERANCE = 2;
|
|
35
|
+
const TARGET_TOP_STABLE_FRAMES = 4;
|
|
36
|
+
const TARGET_TOP_SETTLE_TIMEOUT = 1600;
|
|
37
|
+
const TARGET_TOP_CORRECTION_DELAY = 360;
|
|
38
|
+
const SCROLL_EDGE_TOLERANCE = 2;
|
|
39
|
+
|
|
40
|
+
function waitForNextFrame() {
|
|
41
|
+
return new Promise<void>((resolve) => {
|
|
42
|
+
window.requestAnimationFrame(() => resolve());
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function waitForMessageElement(scrollEl: HTMLElement, messageId: string) {
|
|
47
|
+
let target = getMessageElementInScrollContainer(scrollEl, messageId);
|
|
48
|
+
for (let index = 0; index < 3 && !target; index += 1) {
|
|
49
|
+
await waitForNextFrame();
|
|
50
|
+
target = getMessageElementInScrollContainer(scrollEl, messageId);
|
|
51
|
+
}
|
|
52
|
+
return target;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isScrollAtBoundaryForOffset(scrollEl: HTMLElement, offset: number) {
|
|
56
|
+
if (Math.abs(offset) <= TARGET_TOP_TOLERANCE) return false;
|
|
57
|
+
|
|
58
|
+
const maxScrollDistance = Math.max(scrollEl.scrollHeight - scrollEl.clientHeight, 0);
|
|
59
|
+
const isDescOrder = scrollEl.className.includes('bubble-list-order-desc');
|
|
60
|
+
|
|
61
|
+
if (isDescOrder) {
|
|
62
|
+
const scrollDistance = Math.abs(scrollEl.scrollTop);
|
|
63
|
+
if (offset > 0) return scrollDistance <= SCROLL_EDGE_TOLERANCE;
|
|
64
|
+
return maxScrollDistance - scrollDistance <= SCROLL_EDGE_TOLERANCE;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (offset > 0) {
|
|
68
|
+
return maxScrollDistance - scrollEl.scrollTop <= SCROLL_EDGE_TOLERANCE;
|
|
69
|
+
}
|
|
70
|
+
return scrollEl.scrollTop <= SCROLL_EDGE_TOLERANCE;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function scrollTargetIntoContainerTopAndSettle(
|
|
74
|
+
scrollEl: HTMLElement,
|
|
75
|
+
messageId: string,
|
|
76
|
+
initialTarget: HTMLElement,
|
|
77
|
+
isCurrent: () => boolean,
|
|
78
|
+
) {
|
|
79
|
+
let target = initialTarget;
|
|
80
|
+
let previousOffset: number | undefined;
|
|
81
|
+
let stableFrames = 0;
|
|
82
|
+
const startTime = window.performance.now();
|
|
83
|
+
let topOffset = getScrollVisibleTopOffset(scrollEl);
|
|
84
|
+
|
|
85
|
+
if (!isCurrent()) return target;
|
|
86
|
+
scrollTargetIntoContainerTop(scrollEl, target, 'smooth', topOffset);
|
|
87
|
+
|
|
88
|
+
while (window.performance.now() - startTime < TARGET_TOP_SETTLE_TIMEOUT) {
|
|
89
|
+
await waitForNextFrame();
|
|
90
|
+
if (!isCurrent()) return target;
|
|
91
|
+
|
|
92
|
+
target = getMessageElementInScrollContainer(scrollEl, messageId) || target;
|
|
93
|
+
topOffset = getScrollVisibleTopOffset(scrollEl);
|
|
94
|
+
const offset = getTargetTopOffset(scrollEl, target, topOffset);
|
|
95
|
+
if (Math.abs(offset) <= TARGET_TOP_TOLERANCE) {
|
|
96
|
+
stableFrames += 1;
|
|
97
|
+
if (stableFrames >= TARGET_TOP_STABLE_FRAMES) return target;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isScrollAtBoundaryForOffset(scrollEl, offset)) {
|
|
102
|
+
return target;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stableFrames = 0;
|
|
106
|
+
const elapsed = window.performance.now() - startTime;
|
|
107
|
+
const offsetSettled = previousOffset !== undefined && Math.abs(offset - previousOffset) < 0.5;
|
|
108
|
+
if (elapsed > TARGET_TOP_CORRECTION_DELAY && offsetSettled) {
|
|
109
|
+
scrollTargetIntoContainerTop(scrollEl, target, 'auto', topOffset);
|
|
110
|
+
}
|
|
111
|
+
previousOffset = offset;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!isCurrent()) return target;
|
|
115
|
+
target = getMessageElementInScrollContainer(scrollEl, messageId) || target;
|
|
116
|
+
scrollTargetIntoContainerTop(scrollEl, target, 'auto', getScrollVisibleTopOffset(scrollEl));
|
|
117
|
+
await waitForNextFrame();
|
|
118
|
+
return target;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function getAnchorAttachmentText(anchor: UserMessageAnchor) {
|
|
122
|
+
const countByType = anchor.attachments.reduce<Record<string, number>>((result, attachment) => {
|
|
123
|
+
result[attachment.type] = (result[attachment.type] || 0) + 1;
|
|
124
|
+
return result;
|
|
125
|
+
}, {});
|
|
126
|
+
|
|
127
|
+
return Object.entries(countByType).map(([type, count]) => `${type}:${count}`).join(' · ');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function UserMessageAnchorTooltip(props: {
|
|
131
|
+
anchors: UserMessageAnchor[];
|
|
132
|
+
prefixCls: string;
|
|
133
|
+
}) {
|
|
134
|
+
const { anchors, prefixCls } = props;
|
|
135
|
+
const firstAnchor = anchors[0];
|
|
136
|
+
if (!firstAnchor) return null;
|
|
137
|
+
|
|
138
|
+
if (anchors.length > 1) {
|
|
139
|
+
const previewAnchors = anchors.slice(0, 4);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className={`${prefixCls}-anchor-tooltip`}>
|
|
143
|
+
<div className={`${prefixCls}-anchor-tooltip-text`}>{anchors.length} user messages</div>
|
|
144
|
+
<div className={`${prefixCls}-anchor-tooltip-attachments`}>
|
|
145
|
+
{previewAnchors.map((anchor) => {
|
|
146
|
+
const attachmentText = getAnchorAttachmentText(anchor);
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className={`${prefixCls}-anchor-tooltip-attachment`} key={anchor.id}>
|
|
150
|
+
<span className={`${prefixCls}-anchor-tooltip-attachment-name`}>
|
|
151
|
+
{[getAnchorTimeText(anchor.createdAt), anchor.preview, attachmentText].filter(Boolean).join(' · ')}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
{anchors.length > previewAnchors.length ? (
|
|
157
|
+
<div className={`${prefixCls}-anchor-tooltip-attachment`}>
|
|
158
|
+
<span className={`${prefixCls}-anchor-tooltip-attachment-type`}>More</span>
|
|
159
|
+
<span className={`${prefixCls}-anchor-tooltip-attachment-name`}>
|
|
160
|
+
{anchors.length - previewAnchors.length} more messages
|
|
161
|
+
</span>
|
|
162
|
+
</div>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className={`${prefixCls}-anchor-tooltip`}>
|
|
171
|
+
{firstAnchor.createdAt ? (
|
|
172
|
+
<div className={`${prefixCls}-anchor-tooltip-time`}>{getAnchorTimeText(firstAnchor.createdAt)}</div>
|
|
173
|
+
) : null}
|
|
174
|
+
<div className={`${prefixCls}-anchor-tooltip-text`}>{firstAnchor.preview}</div>
|
|
175
|
+
{firstAnchor.attachments.length ? (
|
|
176
|
+
<div className={`${prefixCls}-anchor-tooltip-attachments`}>
|
|
177
|
+
<div className={`${prefixCls}-anchor-tooltip-attachment`}>
|
|
178
|
+
<span className={`${prefixCls}-anchor-tooltip-attachment-name`}>
|
|
179
|
+
{getAnchorAttachmentText(firstAnchor)}
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
184
|
+
</div>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function UserMessageAnchorDirectory(props: {
|
|
189
|
+
anchors: UserMessageAnchor[];
|
|
190
|
+
activeAnchorId?: string;
|
|
191
|
+
openVersion: number;
|
|
192
|
+
prefixCls: string;
|
|
193
|
+
onAnchorClick: (messageId: string) => void;
|
|
194
|
+
}) {
|
|
195
|
+
const { anchors, activeAnchorId, openVersion, prefixCls, onAnchorClick } = props;
|
|
196
|
+
const activeItemRef = React.useRef<HTMLButtonElement | null>(null);
|
|
197
|
+
const listRef = React.useRef<HTMLDivElement | null>(null);
|
|
198
|
+
|
|
199
|
+
const scrollActiveItemIntoView = useCallback((behavior: ScrollBehavior = 'smooth') => {
|
|
200
|
+
const activeItem = activeItemRef.current;
|
|
201
|
+
const list = listRef.current;
|
|
202
|
+
if (!activeItem || !list) return;
|
|
203
|
+
|
|
204
|
+
const itemOffsetTop = activeItem.offsetTop;
|
|
205
|
+
const nextScrollTop = itemOffsetTop - (list.clientHeight - activeItem.offsetHeight) / 2;
|
|
206
|
+
list.scrollTo({
|
|
207
|
+
top: Math.max(0, nextScrollTop),
|
|
208
|
+
behavior,
|
|
209
|
+
});
|
|
210
|
+
}, []);
|
|
211
|
+
|
|
212
|
+
React.useEffect(() => {
|
|
213
|
+
if (!openVersion) return;
|
|
214
|
+
const frame = window.requestAnimationFrame(() => {
|
|
215
|
+
scrollActiveItemIntoView('auto');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return () => window.cancelAnimationFrame(frame);
|
|
219
|
+
}, [openVersion, scrollActiveItemIntoView]);
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className={`${prefixCls}-anchor-directory`}>
|
|
223
|
+
<div className={`${prefixCls}-anchor-directory-title`}>
|
|
224
|
+
<span>导航 ({anchors.length})</span>
|
|
225
|
+
<button
|
|
226
|
+
aria-label="Scroll directory to active user message"
|
|
227
|
+
className={`${prefixCls}-anchor-directory-locate`}
|
|
228
|
+
disabled={!activeAnchorId}
|
|
229
|
+
onClick={(event) => {
|
|
230
|
+
event.stopPropagation();
|
|
231
|
+
scrollActiveItemIntoView('smooth');
|
|
232
|
+
}}
|
|
233
|
+
type="button"
|
|
234
|
+
>
|
|
235
|
+
<LocateFixed size={14} />
|
|
236
|
+
</button>
|
|
237
|
+
</div>
|
|
238
|
+
<div className={`${prefixCls}-anchor-directory-list`} ref={listRef}>
|
|
239
|
+
{anchors.map((anchor) => {
|
|
240
|
+
const timeText = getAnchorTimeText(anchor.createdAt);
|
|
241
|
+
const attachmentText = getAnchorAttachmentText(anchor);
|
|
242
|
+
const active = anchor.id === activeAnchorId;
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<button
|
|
246
|
+
className={cls(`${prefixCls}-anchor-directory-item`, {
|
|
247
|
+
[`${prefixCls}-anchor-directory-item-active`]: active,
|
|
248
|
+
})}
|
|
249
|
+
key={anchor.id}
|
|
250
|
+
onClick={() => onAnchorClick(anchor.id)}
|
|
251
|
+
ref={active ? activeItemRef : undefined}
|
|
252
|
+
type="button"
|
|
253
|
+
>
|
|
254
|
+
<span className={`${prefixCls}-anchor-directory-main`}>
|
|
255
|
+
<span className={`${prefixCls}-anchor-directory-message`}>{anchor.preview}</span>
|
|
256
|
+
{timeText ? (
|
|
257
|
+
<span className={`${prefixCls}-anchor-directory-time`}>{timeText}</span>
|
|
258
|
+
) : null}
|
|
259
|
+
</span>
|
|
260
|
+
{attachmentText ? (
|
|
261
|
+
<span className={`${prefixCls}-anchor-directory-attachments`}>{attachmentText}</span>
|
|
262
|
+
) : null}
|
|
263
|
+
</button>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getAnchorGroups(
|
|
272
|
+
anchors: UserMessageAnchor[],
|
|
273
|
+
anchorPositions: Record<string, number>,
|
|
274
|
+
activeAnchorId: string | undefined,
|
|
275
|
+
minGap: number,
|
|
276
|
+
trackHeight: number,
|
|
277
|
+
): UserMessageAnchorGroup[] {
|
|
278
|
+
const minGapPercent = trackHeight > 0 ? (minGap / trackHeight) * 100 : 0;
|
|
279
|
+
const groups: Array<UserMessageAnchorGroup & { lastTop: number }> = [];
|
|
280
|
+
|
|
281
|
+
anchors.forEach((anchor) => {
|
|
282
|
+
const top = anchorPositions[anchor.id] ?? anchor.orderPercent;
|
|
283
|
+
const lastGroup = groups[groups.length - 1];
|
|
284
|
+
|
|
285
|
+
if (lastGroup && Math.abs(top - lastGroup.lastTop) < minGapPercent) {
|
|
286
|
+
lastGroup.anchors.push(anchor);
|
|
287
|
+
lastGroup.lastTop = top;
|
|
288
|
+
lastGroup.top = (lastGroup.top * (lastGroup.anchors.length - 1) + top) / lastGroup.anchors.length;
|
|
289
|
+
if (anchor.id === activeAnchorId) {
|
|
290
|
+
lastGroup.active = true;
|
|
291
|
+
lastGroup.targetAnchor = anchor;
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
groups.push({
|
|
297
|
+
active: anchor.id === activeAnchorId,
|
|
298
|
+
anchors: [anchor],
|
|
299
|
+
id: anchor.id,
|
|
300
|
+
lastTop: top,
|
|
301
|
+
targetAnchor: anchor,
|
|
302
|
+
top,
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
return groups.map((group) => {
|
|
307
|
+
const id = group.anchors.length > 1
|
|
308
|
+
? `${group.anchors[0].id}-${group.anchors[group.anchors.length - 1].id}`
|
|
309
|
+
: group.id;
|
|
310
|
+
let targetAnchor = group.targetAnchor;
|
|
311
|
+
|
|
312
|
+
if (group.anchors.length > 1) {
|
|
313
|
+
const middleAnchor = group.anchors[Math.floor((group.anchors.length - 1) / 2)] || group.targetAnchor;
|
|
314
|
+
targetAnchor = group.active ? group.targetAnchor : middleAnchor;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
active: group.active,
|
|
319
|
+
anchors: group.anchors,
|
|
320
|
+
id,
|
|
321
|
+
targetAnchor,
|
|
322
|
+
top: group.top,
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default function UserMessageAnchors(props: UserMessageAnchorsProps) {
|
|
328
|
+
const {
|
|
329
|
+
badgeMaxCount,
|
|
330
|
+
enabled = true,
|
|
331
|
+
items,
|
|
332
|
+
minGap,
|
|
333
|
+
minCount,
|
|
334
|
+
prefixCls,
|
|
335
|
+
renderedItemsKey,
|
|
336
|
+
scrollContainerClassName,
|
|
337
|
+
variant = 'minimap',
|
|
338
|
+
onEnsureMessageVisible,
|
|
339
|
+
} = props;
|
|
340
|
+
const anchorTrackRef = React.useRef<HTMLElement | null>(null);
|
|
341
|
+
const activeAnchorLockRef = React.useRef<string | undefined>();
|
|
342
|
+
const frameRef = React.useRef<number | undefined>();
|
|
343
|
+
const scrollSequenceRef = React.useRef(0);
|
|
344
|
+
const [anchorPositions, setAnchorPositions] = useState<Record<string, number>>({});
|
|
345
|
+
const [activeAnchorId, setActiveAnchorId] = useState<string | undefined>();
|
|
346
|
+
const [anchorRight, setAnchorRight] = useState<number | undefined>();
|
|
347
|
+
const [directoryOpenVersion, setDirectoryOpenVersion] = useState(0);
|
|
348
|
+
const [trackHeight, setTrackHeight] = useState(0);
|
|
349
|
+
const normalizedMinGap = useMemo(() => getUserMessageAnchorMinGap(minGap), [minGap]);
|
|
350
|
+
const normalizedMinCount = useMemo(() => getUserMessageAnchorMinCount(minCount), [minCount]);
|
|
351
|
+
|
|
352
|
+
const anchors = useMemo<UserMessageAnchor[]>(() => {
|
|
353
|
+
if (!enabled) return [];
|
|
354
|
+
|
|
355
|
+
const visualItems = items.slice().reverse();
|
|
356
|
+
const totalCount = Math.max(visualItems.length, 1);
|
|
357
|
+
|
|
358
|
+
return visualItems.reduce<UserMessageAnchor[]>((result, message, index) => {
|
|
359
|
+
const anchor = getUserMessageAnchor(message);
|
|
360
|
+
if (!anchor) return result;
|
|
361
|
+
|
|
362
|
+
result.push({
|
|
363
|
+
...anchor,
|
|
364
|
+
orderPercent: ((index + 0.5) / totalCount) * 100,
|
|
365
|
+
});
|
|
366
|
+
return result;
|
|
367
|
+
}, []);
|
|
368
|
+
}, [enabled, items]);
|
|
369
|
+
const anchorIdsKey = useMemo(() => anchors.map((anchor) => anchor.id).join('|'), [anchors]);
|
|
370
|
+
const visible = enabled && anchors.length > 0 && anchors.length >= normalizedMinCount;
|
|
371
|
+
|
|
372
|
+
const measureAnchors = useCallback(() => {
|
|
373
|
+
const root = anchorTrackRef.current?.closest(`.${prefixCls}`);
|
|
374
|
+
const scrollEl = root?.querySelector(`.${scrollContainerClassName}`) as HTMLElement | null;
|
|
375
|
+
if (!scrollEl) return;
|
|
376
|
+
|
|
377
|
+
const rootRect = root?.getBoundingClientRect();
|
|
378
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
379
|
+
if (rootRect) {
|
|
380
|
+
const trackWidth = variant === 'navigator' ? NAVIGATOR_TRACK_WIDTH : ANCHOR_TRACK_WIDTH;
|
|
381
|
+
const nextAnchorRight = Math.round(Math.max(
|
|
382
|
+
12,
|
|
383
|
+
rootRect.right - scrollRect.right - ANCHOR_CONTENT_GAP - trackWidth,
|
|
384
|
+
));
|
|
385
|
+
setAnchorRight((prev) => prev === nextAnchorRight ? prev : nextAnchorRight);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const lockedAnchorId = activeAnchorLockRef.current;
|
|
389
|
+
const lockedAnchorExists = !!lockedAnchorId && anchors.some((anchor) => anchor.id === lockedAnchorId);
|
|
390
|
+
if (lockedAnchorId && !lockedAnchorExists) {
|
|
391
|
+
activeAnchorLockRef.current = undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const visibleTopOffset = getScrollVisibleTopOffset(scrollEl);
|
|
395
|
+
const nextActiveAnchorId = lockedAnchorExists
|
|
396
|
+
? lockedAnchorId
|
|
397
|
+
: getActiveAnchorId(scrollEl, anchors, visibleTopOffset);
|
|
398
|
+
setActiveAnchorId((prev) => prev === nextActiveAnchorId ? prev : nextActiveAnchorId);
|
|
399
|
+
|
|
400
|
+
if (variant === 'navigator') {
|
|
401
|
+
setAnchorPositions((prev) => Object.keys(prev).length ? {} : prev);
|
|
402
|
+
setTrackHeight((prev) => prev === 0 ? prev : 0);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const nextTrackHeight = anchorTrackRef.current?.clientHeight || 0;
|
|
407
|
+
setTrackHeight((prev) => Math.abs(prev - nextTrackHeight) < 1 ? prev : nextTrackHeight);
|
|
408
|
+
|
|
409
|
+
const targetMap = getMessageElementMapInScrollContainer(scrollEl);
|
|
410
|
+
const scrollHeight = Math.max(scrollEl.scrollHeight, 1);
|
|
411
|
+
const maxScrollTop = Math.max(scrollEl.scrollHeight - scrollEl.clientHeight, 0);
|
|
412
|
+
const visualScrollTop = maxScrollTop + scrollEl.scrollTop;
|
|
413
|
+
const nextPositions = anchors.reduce<Record<string, number>>((result, anchor) => {
|
|
414
|
+
const target = targetMap.get(anchor.id);
|
|
415
|
+
if (!target) {
|
|
416
|
+
result[anchor.id] = anchor.orderPercent;
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const targetRect = target.getBoundingClientRect();
|
|
421
|
+
const viewportCenter = targetRect.top - scrollRect.top + targetRect.height / 2;
|
|
422
|
+
const visualCenter = visualScrollTop + viewportCenter;
|
|
423
|
+
result[anchor.id] = clamp((visualCenter / scrollHeight) * 100, 0, 100);
|
|
424
|
+
return result;
|
|
425
|
+
}, {});
|
|
426
|
+
|
|
427
|
+
setAnchorPositions((prev) => {
|
|
428
|
+
if (areAnchorPositionsEqual(prev, nextPositions)) return prev;
|
|
429
|
+
return nextPositions;
|
|
430
|
+
});
|
|
431
|
+
}, [anchors, prefixCls, scrollContainerClassName, variant]);
|
|
432
|
+
|
|
433
|
+
const updateAnchors = useCallback(() => {
|
|
434
|
+
if (frameRef.current) {
|
|
435
|
+
window.cancelAnimationFrame(frameRef.current);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
frameRef.current = window.requestAnimationFrame(() => {
|
|
439
|
+
frameRef.current = undefined;
|
|
440
|
+
measureAnchors();
|
|
441
|
+
});
|
|
442
|
+
}, [measureAnchors]);
|
|
443
|
+
|
|
444
|
+
const cancelPendingAnchorPositionUpdate = useCallback(() => {
|
|
445
|
+
if (frameRef.current) {
|
|
446
|
+
window.cancelAnimationFrame(frameRef.current);
|
|
447
|
+
frameRef.current = undefined;
|
|
448
|
+
}
|
|
449
|
+
}, []);
|
|
450
|
+
|
|
451
|
+
React.useEffect(() => {
|
|
452
|
+
cancelPendingAnchorPositionUpdate();
|
|
453
|
+
if (!visible) {
|
|
454
|
+
activeAnchorLockRef.current = undefined;
|
|
455
|
+
setAnchorPositions((prev) => Object.keys(prev).length ? {} : prev);
|
|
456
|
+
setActiveAnchorId((prev) => prev === undefined ? prev : undefined);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
measureAnchors();
|
|
461
|
+
|
|
462
|
+
const root = anchorTrackRef.current?.closest(`.${prefixCls}`);
|
|
463
|
+
const scrollEl = root?.querySelector(`.${scrollContainerClassName}`) as HTMLElement | null;
|
|
464
|
+
const resizeObserver = typeof ResizeObserver !== 'undefined'
|
|
465
|
+
? new ResizeObserver(updateAnchors)
|
|
466
|
+
: undefined;
|
|
467
|
+
|
|
468
|
+
if (scrollEl) resizeObserver?.observe(scrollEl);
|
|
469
|
+
if (variant !== 'navigator') {
|
|
470
|
+
const targetMap = scrollEl ? getMessageElementMapInScrollContainer(scrollEl) : new Map<string, HTMLElement>();
|
|
471
|
+
const scrollChildren = Array.from(scrollEl?.children || [])
|
|
472
|
+
.filter((target): target is HTMLElement => target instanceof HTMLElement);
|
|
473
|
+
scrollChildren.forEach((target) => resizeObserver?.observe(target));
|
|
474
|
+
targetMap.forEach((target) => resizeObserver?.observe(target));
|
|
475
|
+
}
|
|
476
|
+
scrollEl?.addEventListener('scroll', updateAnchors, { passive: true });
|
|
477
|
+
window.addEventListener('resize', updateAnchors);
|
|
478
|
+
|
|
479
|
+
return () => {
|
|
480
|
+
cancelPendingAnchorPositionUpdate();
|
|
481
|
+
resizeObserver?.disconnect();
|
|
482
|
+
scrollEl?.removeEventListener('scroll', updateAnchors);
|
|
483
|
+
window.removeEventListener('resize', updateAnchors);
|
|
484
|
+
};
|
|
485
|
+
}, [
|
|
486
|
+
anchorIdsKey,
|
|
487
|
+
cancelPendingAnchorPositionUpdate,
|
|
488
|
+
measureAnchors,
|
|
489
|
+
prefixCls,
|
|
490
|
+
renderedItemsKey,
|
|
491
|
+
scrollContainerClassName,
|
|
492
|
+
updateAnchors,
|
|
493
|
+
variant,
|
|
494
|
+
visible,
|
|
495
|
+
]);
|
|
496
|
+
|
|
497
|
+
const handleAnchorClick = useCallback(async (messageId: string) => {
|
|
498
|
+
const scrollSequence = scrollSequenceRef.current + 1;
|
|
499
|
+
scrollSequenceRef.current = scrollSequence;
|
|
500
|
+
activeAnchorLockRef.current = messageId;
|
|
501
|
+
setActiveAnchorId(messageId);
|
|
502
|
+
|
|
503
|
+
try {
|
|
504
|
+
await onEnsureMessageVisible(messageId);
|
|
505
|
+
} catch {
|
|
506
|
+
if (scrollSequenceRef.current === scrollSequence) {
|
|
507
|
+
activeAnchorLockRef.current = undefined;
|
|
508
|
+
measureAnchors();
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
cancelPendingAnchorPositionUpdate();
|
|
513
|
+
|
|
514
|
+
const root = anchorTrackRef.current?.closest(`.${prefixCls}`);
|
|
515
|
+
const scrollEl = root?.querySelector(`.${scrollContainerClassName}`) as HTMLElement | null;
|
|
516
|
+
if (!scrollEl) {
|
|
517
|
+
if (scrollSequenceRef.current === scrollSequence) {
|
|
518
|
+
activeAnchorLockRef.current = undefined;
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const target = await waitForMessageElement(scrollEl, messageId);
|
|
524
|
+
if (!target) {
|
|
525
|
+
if (scrollSequenceRef.current === scrollSequence) {
|
|
526
|
+
activeAnchorLockRef.current = undefined;
|
|
527
|
+
measureAnchors();
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
await waitForNextFrame();
|
|
533
|
+
if (scrollSequenceRef.current !== scrollSequence) return;
|
|
534
|
+
|
|
535
|
+
const targetAtTop = await scrollTargetIntoContainerTopAndSettle(
|
|
536
|
+
scrollEl,
|
|
537
|
+
messageId,
|
|
538
|
+
target,
|
|
539
|
+
() => scrollSequenceRef.current === scrollSequence,
|
|
540
|
+
);
|
|
541
|
+
if (scrollSequenceRef.current !== scrollSequence) return;
|
|
542
|
+
|
|
543
|
+
setActiveAnchorId(messageId);
|
|
544
|
+
cancelPendingAnchorPositionUpdate();
|
|
545
|
+
targetAtTop.classList.remove(`${prefixCls}-anchor-target-active`);
|
|
546
|
+
window.requestAnimationFrame(() => {
|
|
547
|
+
targetAtTop.classList.add(`${prefixCls}-anchor-target-active`);
|
|
548
|
+
window.setTimeout(() => {
|
|
549
|
+
targetAtTop.classList.remove(`${prefixCls}-anchor-target-active`);
|
|
550
|
+
if (scrollSequenceRef.current === scrollSequence) {
|
|
551
|
+
activeAnchorLockRef.current = undefined;
|
|
552
|
+
measureAnchors();
|
|
553
|
+
}
|
|
554
|
+
}, TARGET_ACTIVE_DURATION);
|
|
555
|
+
});
|
|
556
|
+
}, [
|
|
557
|
+
cancelPendingAnchorPositionUpdate,
|
|
558
|
+
measureAnchors,
|
|
559
|
+
onEnsureMessageVisible,
|
|
560
|
+
prefixCls,
|
|
561
|
+
scrollContainerClassName,
|
|
562
|
+
]);
|
|
563
|
+
|
|
564
|
+
if (!visible) return null;
|
|
565
|
+
|
|
566
|
+
const activeAnchorIndex = activeAnchorId
|
|
567
|
+
? anchors.findIndex((anchor) => anchor.id === activeAnchorId)
|
|
568
|
+
: anchors.length - 1;
|
|
569
|
+
const normalizedActiveAnchorIndex = activeAnchorIndex >= 0 ? activeAnchorIndex : anchors.length - 1;
|
|
570
|
+
const previousAnchor = normalizedActiveAnchorIndex > 0 ? anchors[normalizedActiveAnchorIndex - 1] : undefined;
|
|
571
|
+
const nextAnchor = normalizedActiveAnchorIndex < anchors.length - 1
|
|
572
|
+
? anchors[normalizedActiveAnchorIndex + 1]
|
|
573
|
+
: undefined;
|
|
574
|
+
|
|
575
|
+
if (variant === 'navigator') {
|
|
576
|
+
return (
|
|
577
|
+
<nav
|
|
578
|
+
aria-label="User message navigation"
|
|
579
|
+
className={cls(`${prefixCls}-anchors`, `${prefixCls}-anchors-navigator`)}
|
|
580
|
+
ref={anchorTrackRef}
|
|
581
|
+
style={anchorRight === undefined ? undefined : { right: anchorRight }}
|
|
582
|
+
>
|
|
583
|
+
<button
|
|
584
|
+
aria-label="Scroll to previous user message"
|
|
585
|
+
className={`${prefixCls}-anchor-nav-button`}
|
|
586
|
+
disabled={!previousAnchor}
|
|
587
|
+
onClick={() => previousAnchor && handleAnchorClick(previousAnchor.id)}
|
|
588
|
+
type="button"
|
|
589
|
+
>
|
|
590
|
+
<ChevronUp size={18} />
|
|
591
|
+
</button>
|
|
592
|
+
<Popover
|
|
593
|
+
content={(
|
|
594
|
+
<UserMessageAnchorDirectory
|
|
595
|
+
activeAnchorId={activeAnchorId}
|
|
596
|
+
anchors={anchors}
|
|
597
|
+
onAnchorClick={handleAnchorClick}
|
|
598
|
+
openVersion={directoryOpenVersion}
|
|
599
|
+
prefixCls={prefixCls}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
602
|
+
onOpenChange={(open) => {
|
|
603
|
+
if (open) setDirectoryOpenVersion((prev) => prev + 1);
|
|
604
|
+
}}
|
|
605
|
+
overlayClassName={`${prefixCls}-anchor-directory-popover`}
|
|
606
|
+
placement="left"
|
|
607
|
+
styles={{ body: { padding: 0 } }}
|
|
608
|
+
trigger="hover"
|
|
609
|
+
>
|
|
610
|
+
<button
|
|
611
|
+
aria-current={activeAnchorId ? 'location' : undefined}
|
|
612
|
+
aria-label="Open user message directory"
|
|
613
|
+
className={cls(`${prefixCls}-anchor-nav-button`, `${prefixCls}-anchor-nav-button-menu`, {
|
|
614
|
+
[`${prefixCls}-anchor-nav-button-active`]: !!activeAnchorId,
|
|
615
|
+
})}
|
|
616
|
+
type="button"
|
|
617
|
+
>
|
|
618
|
+
<ListIcon size={18} />
|
|
619
|
+
<span className={`${prefixCls}-anchor-nav-count`}>
|
|
620
|
+
{getUserMessageAnchorBadgeText(anchors.length, badgeMaxCount)}
|
|
621
|
+
</span>
|
|
622
|
+
</button>
|
|
623
|
+
</Popover>
|
|
624
|
+
<button
|
|
625
|
+
aria-label="Scroll to next user message"
|
|
626
|
+
className={`${prefixCls}-anchor-nav-button`}
|
|
627
|
+
disabled={!nextAnchor}
|
|
628
|
+
onClick={() => nextAnchor && handleAnchorClick(nextAnchor.id)}
|
|
629
|
+
type="button"
|
|
630
|
+
>
|
|
631
|
+
<ChevronDown size={18} />
|
|
632
|
+
</button>
|
|
633
|
+
</nav>
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const anchorGroups = getAnchorGroups(anchors, anchorPositions, activeAnchorId, normalizedMinGap, trackHeight);
|
|
638
|
+
|
|
639
|
+
return (
|
|
640
|
+
<nav
|
|
641
|
+
aria-label="User message anchors"
|
|
642
|
+
className={`${prefixCls}-anchors`}
|
|
643
|
+
ref={anchorTrackRef}
|
|
644
|
+
style={anchorRight === undefined ? undefined : { right: anchorRight }}
|
|
645
|
+
>
|
|
646
|
+
{anchorGroups.map((group) => {
|
|
647
|
+
return (
|
|
648
|
+
<Tooltip
|
|
649
|
+
key={group.id}
|
|
650
|
+
placement="left"
|
|
651
|
+
title={<UserMessageAnchorTooltip anchors={group.anchors} prefixCls={prefixCls} />}
|
|
652
|
+
>
|
|
653
|
+
<button
|
|
654
|
+
aria-label={group.anchors.length > 1
|
|
655
|
+
? `Scroll to ${group.anchors.length} grouped user messages`
|
|
656
|
+
: `Scroll to user message: ${group.targetAnchor.preview}`}
|
|
657
|
+
aria-current={group.active ? 'location' : undefined}
|
|
658
|
+
className={cls(`${prefixCls}-anchor`, {
|
|
659
|
+
[`${prefixCls}-anchor-active`]: group.active,
|
|
660
|
+
[`${prefixCls}-anchor-grouped`]: group.anchors.length > 1,
|
|
661
|
+
})}
|
|
662
|
+
onClick={() => handleAnchorClick(group.targetAnchor.id)}
|
|
663
|
+
style={{ top: `${group.top}%` }}
|
|
664
|
+
type="button"
|
|
665
|
+
>
|
|
666
|
+
<span className={`${prefixCls}-anchor-bar`} />
|
|
667
|
+
</button>
|
|
668
|
+
</Tooltip>
|
|
669
|
+
);
|
|
670
|
+
})}
|
|
671
|
+
</nav>
|
|
672
|
+
);
|
|
673
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IAgentScopeRuntimeWebUIMessage } from "../../../types/IMessages";
|
|
2
|
+
|
|
3
|
+
export type AttachmentPreview = {
|
|
4
|
+
type: string;
|
|
5
|
+
name: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type UserMessageAnchor = {
|
|
9
|
+
id: string;
|
|
10
|
+
preview: string;
|
|
11
|
+
attachments: AttachmentPreview[];
|
|
12
|
+
createdAt?: number;
|
|
13
|
+
orderPercent: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type UserMessageAnchorsProps = {
|
|
17
|
+
badgeMaxCount?: number;
|
|
18
|
+
enabled?: boolean;
|
|
19
|
+
items: IAgentScopeRuntimeWebUIMessage[];
|
|
20
|
+
minGap?: number;
|
|
21
|
+
minCount?: number;
|
|
22
|
+
prefixCls: string;
|
|
23
|
+
renderedItemsKey: string;
|
|
24
|
+
scrollContainerClassName: string;
|
|
25
|
+
variant?: 'minimap' | 'navigator';
|
|
26
|
+
onEnsureMessageVisible: (messageId: string) => Promise<void>;
|
|
27
|
+
};
|