@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
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import { useCallback
|
|
1
|
+
import { useCallback } from "react";
|
|
2
2
|
import { useProviderContext, ChatInput, Disclaimer } from '@agentscope-ai/chat';
|
|
3
3
|
import { useChatAnywhereOptions } from "../../Context/ChatAnywhereOptionsContext";
|
|
4
4
|
import { useGetState } from 'ahooks';
|
|
5
5
|
import { useChatAnywhereInput } from "../../Context/ChatAnywhereInputContext";
|
|
6
6
|
import useAttachments from "./useAttachments";
|
|
7
7
|
import { IAgentScopeRuntimeWebUIInputData } from "@agentscope-ai/chat";
|
|
8
|
-
import type { QueueEnqueueResult, QueuedInputItem } from "../InputQueue";
|
|
9
8
|
|
|
10
9
|
export interface InputProps {
|
|
11
10
|
onCancel: () => void;
|
|
12
11
|
onSubmit: (data: IAgentScopeRuntimeWebUIInputData) => void;
|
|
13
|
-
queue?: {
|
|
14
|
-
items: QueuedInputItem[];
|
|
15
|
-
isOwner: boolean;
|
|
16
|
-
panel?: ReactNode;
|
|
17
|
-
onEnqueue: (data: IAgentScopeRuntimeWebUIInputData) => QueueEnqueueResult | Promise<QueueEnqueueResult>;
|
|
18
|
-
};
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
export default function Input(props: InputProps) {
|
|
@@ -28,6 +21,9 @@ export default function Input(props: InputProps) {
|
|
|
28
21
|
placeholder = '',
|
|
29
22
|
disclaimer = '',
|
|
30
23
|
maxLength,
|
|
24
|
+
showCharacterCount,
|
|
25
|
+
characterCountRender,
|
|
26
|
+
actionAffix,
|
|
31
27
|
beforeSubmit = () => Promise.resolve(true),
|
|
32
28
|
beforeUI,
|
|
33
29
|
afterUI,
|
|
@@ -41,87 +37,29 @@ export default function Input(props: InputProps) {
|
|
|
41
37
|
getFileList,
|
|
42
38
|
setFileList,
|
|
43
39
|
handlePasteFile,
|
|
40
|
+
handleDropFile,
|
|
44
41
|
uploadIconButton,
|
|
45
42
|
uploadFileListHeader
|
|
46
43
|
} = useAttachments(attachments, { disabled: !!inputContext.disabled });
|
|
47
44
|
|
|
48
|
-
const getSubmittableData = useCallback(() => {
|
|
49
|
-
const fileList = (getFileList?.() || []).filter(i => i.response?.url);
|
|
50
|
-
const query = getContent();
|
|
51
|
-
if (!query.trim() && fileList.length === 0) return;
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
query,
|
|
55
|
-
text: query,
|
|
56
|
-
fileList,
|
|
57
|
-
attachments: fileList,
|
|
58
|
-
};
|
|
59
|
-
}, [getContent, getFileList]);
|
|
60
|
-
|
|
61
|
-
const clearInput = useCallback(() => {
|
|
62
|
-
setContent('');
|
|
63
|
-
setFileList?.([]);
|
|
64
|
-
}, [setContent, setFileList]);
|
|
65
|
-
|
|
66
|
-
const handleEnqueue = useCallback(async () => {
|
|
67
|
-
const next = await beforeSubmit();
|
|
68
|
-
if (!next) return;
|
|
69
|
-
|
|
70
|
-
const data = getSubmittableData();
|
|
71
|
-
if (!data) return;
|
|
72
|
-
if (!props.queue) return;
|
|
73
|
-
|
|
74
|
-
const result = await props.queue.onEnqueue(data);
|
|
75
|
-
if (result.ok) {
|
|
76
|
-
clearInput();
|
|
77
|
-
}
|
|
78
|
-
}, [beforeSubmit, clearInput, getSubmittableData, props.queue]);
|
|
79
45
|
|
|
80
46
|
const handleSubmit = useCallback(async () => {
|
|
81
47
|
const next = await beforeSubmit();
|
|
82
48
|
if (!next) return;
|
|
83
49
|
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
props.queue.items.length ||
|
|
90
|
-
props.queue.isOwner === false
|
|
91
|
-
)) {
|
|
92
|
-
const result = await props.queue.onEnqueue(data);
|
|
93
|
-
if (result.ok) {
|
|
94
|
-
clearInput();
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
props.onSubmit(data);
|
|
98
|
-
clearInput();
|
|
99
|
-
}
|
|
100
|
-
}, [beforeSubmit, clearInput, getSubmittableData, inputContext.loading, props.onSubmit, props.queue]);
|
|
101
|
-
|
|
102
|
-
const handleKeyDownCapture = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
103
|
-
if (event.key !== 'Enter' || event.shiftKey) return;
|
|
104
|
-
if (event.nativeEvent?.isComposing) return;
|
|
105
|
-
if (!props.queue) return;
|
|
106
|
-
const forceEnqueue = event.ctrlKey || event.metaKey;
|
|
107
|
-
if (!forceEnqueue && !inputContext.loading && !props.queue?.items.length) return;
|
|
108
|
-
|
|
109
|
-
const data = getSubmittableData();
|
|
110
|
-
if (!data) return;
|
|
111
|
-
|
|
112
|
-
event.preventDefault();
|
|
113
|
-
event.stopPropagation();
|
|
114
|
-
void (forceEnqueue ? handleEnqueue() : handleSubmit());
|
|
115
|
-
}, [getSubmittableData, handleEnqueue, handleSubmit, inputContext.loading, props.queue, props.queue?.items.length]);
|
|
50
|
+
const fileList = (getFileList?.() || []).filter(i => i.response?.url);
|
|
51
|
+
props.onSubmit({ query: getContent(), fileList });
|
|
52
|
+
setContent('');
|
|
53
|
+
setFileList?.([]);
|
|
54
|
+
}, []);
|
|
116
55
|
|
|
117
56
|
const handleCancel = useCallback(() => {
|
|
118
57
|
props.onCancel();
|
|
119
|
-
}, [
|
|
58
|
+
}, []);
|
|
120
59
|
|
|
121
|
-
return <div className={prefixCls}
|
|
60
|
+
return <div className={prefixCls}>
|
|
122
61
|
<div className={`${prefixCls}-wrapper`}>
|
|
123
62
|
{beforeUI}
|
|
124
|
-
{props.queue?.panel}
|
|
125
63
|
<ChatInput
|
|
126
64
|
loading={inputContext.loading}
|
|
127
65
|
disabled={inputContext.disabled}
|
|
@@ -134,11 +72,14 @@ export default function Input(props: InputProps) {
|
|
|
134
72
|
header={uploadFileListHeader}
|
|
135
73
|
onChange={setContent}
|
|
136
74
|
maxLength={maxLength}
|
|
75
|
+
showCharacterCount={showCharacterCount}
|
|
76
|
+
characterCountRender={characterCountRender}
|
|
77
|
+
actionAffix={actionAffix}
|
|
137
78
|
onSubmit={handleSubmit}
|
|
138
79
|
onCancel={handleCancel}
|
|
139
|
-
allowSubmitWhenLoading={!!props.queue}
|
|
140
80
|
allowSpeech={allowSpeech}
|
|
141
81
|
onPasteFile={handlePasteFile}
|
|
82
|
+
onDropFile={handleDropFile}
|
|
142
83
|
suggestions={suggestions}
|
|
143
84
|
/>
|
|
144
85
|
{afterUI}
|
|
@@ -6,6 +6,34 @@ import { SparkAttachmentLine } from "@agentscope-ai/icons";
|
|
|
6
6
|
import { Sender, Attachments } from '@agentscope-ai/chat';
|
|
7
7
|
import React, { useCallback, useRef, useState } from "react";
|
|
8
8
|
|
|
9
|
+
function isAcceptedFile(file: File, accept?: string) {
|
|
10
|
+
if (!accept) return true;
|
|
11
|
+
|
|
12
|
+
const fileType = file.type || '';
|
|
13
|
+
const fileName = file.name || '';
|
|
14
|
+
|
|
15
|
+
return accept.split(',').some(pattern => {
|
|
16
|
+
const trimmed = pattern.trim();
|
|
17
|
+
if (!trimmed) return false;
|
|
18
|
+
if (trimmed.startsWith('.')) return fileName.toLowerCase().endsWith(trimmed.toLowerCase());
|
|
19
|
+
if (trimmed === '*/*') return true;
|
|
20
|
+
if (trimmed.includes('*')) {
|
|
21
|
+
const [acceptMain] = trimmed.split('/');
|
|
22
|
+
const [fileMain] = fileType.split('/');
|
|
23
|
+
return acceptMain === fileMain;
|
|
24
|
+
}
|
|
25
|
+
return fileType === trimmed;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFileExtension(fileName: string, fileType: string) {
|
|
30
|
+
const nameMatch = fileName.match(/\.([^.]+)$/);
|
|
31
|
+
if (nameMatch) return nameMatch[1].toLowerCase();
|
|
32
|
+
|
|
33
|
+
const typeMatch = fileType.match(/\/([^/+]+)/);
|
|
34
|
+
return typeMatch ? typeMatch[1].toLowerCase() : 'bin';
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
export default function useAttachments(
|
|
10
38
|
attachments: IAgentScopeRuntimeWebUISenderAttachmentsOptions,
|
|
11
39
|
options?: {
|
|
@@ -16,107 +44,142 @@ export default function useAttachments(
|
|
|
16
44
|
const fileListRef = useRef<UploadFile[]>([]);
|
|
17
45
|
fileListRef.current = fileList;
|
|
18
46
|
|
|
47
|
+
const setMergedFileList = useCallback((nextFileList: React.SetStateAction<UploadFile[]>) => {
|
|
48
|
+
const resolvedFileList = typeof nextFileList === 'function'
|
|
49
|
+
? (nextFileList as (prev: UploadFile[]) => UploadFile[])(fileListRef.current)
|
|
50
|
+
: nextFileList;
|
|
51
|
+
|
|
52
|
+
fileListRef.current = resolvedFileList;
|
|
53
|
+
setFileList(resolvedFileList);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
19
56
|
const getFileList = useCallback(() => fileListRef.current, []);
|
|
20
57
|
|
|
21
58
|
const { trigger, ...rest } = attachments || {};
|
|
59
|
+
const {
|
|
60
|
+
accept,
|
|
61
|
+
beforeUpload,
|
|
62
|
+
customRequest,
|
|
63
|
+
maxCount,
|
|
64
|
+
onChange,
|
|
65
|
+
} = rest;
|
|
66
|
+
const disabled = !!options?.disabled;
|
|
22
67
|
const uidCounter = useRef(0);
|
|
23
68
|
|
|
24
|
-
const
|
|
25
|
-
if (!
|
|
69
|
+
const handleManualFile = useCallback((file: File) => {
|
|
70
|
+
if (disabled || !customRequest || !isAcceptedFile(file, accept)) return;
|
|
71
|
+
if (maxCount && fileListRef.current.length >= maxCount) return;
|
|
26
72
|
|
|
27
73
|
const fileType = file.type || '';
|
|
28
74
|
const fileName = file.name || '';
|
|
29
75
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
76
|
+
const startUpload = (fileToUpload: File | Blob) => {
|
|
77
|
+
if (maxCount && fileListRef.current.length >= maxCount) return;
|
|
78
|
+
|
|
79
|
+
const uploadType = fileToUpload.type || fileType;
|
|
80
|
+
const timestamp = Date.now();
|
|
81
|
+
const uid = `manual-${timestamp}-${uidCounter.current++}`;
|
|
82
|
+
const uploadFile: UploadFile = {
|
|
83
|
+
uid,
|
|
84
|
+
name: fileName || `file-${timestamp}.${getFileExtension(fileName, uploadType)}`,
|
|
85
|
+
size: fileToUpload.size,
|
|
86
|
+
type: uploadType,
|
|
87
|
+
status: 'uploading',
|
|
88
|
+
percent: 0,
|
|
89
|
+
originFileObj: fileToUpload as any,
|
|
90
|
+
};
|
|
45
91
|
|
|
46
|
-
|
|
92
|
+
setMergedFileList(prev => [...prev, uploadFile]);
|
|
93
|
+
|
|
94
|
+
if (uploadType.startsWith('image/')) {
|
|
95
|
+
const reader = new FileReader();
|
|
96
|
+
reader.onload = (e) => {
|
|
97
|
+
const dataUrl = e.target?.result;
|
|
98
|
+
if (typeof dataUrl === 'string') {
|
|
99
|
+
setMergedFileList(prev => prev.map(f =>
|
|
100
|
+
f.uid === uid ? { ...f, thumbUrl: dataUrl } : f
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
reader.readAsDataURL(fileToUpload);
|
|
105
|
+
}
|
|
47
106
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
customRequest(
|
|
108
|
+
{
|
|
109
|
+
file: fileToUpload as any,
|
|
110
|
+
filename: 'file',
|
|
111
|
+
action: '',
|
|
112
|
+
method: 'POST',
|
|
113
|
+
onSuccess: (response: any) => {
|
|
114
|
+
setMergedFileList(prev => prev.map(f =>
|
|
115
|
+
f.uid === uid ? { ...f, status: 'done' as const, response, percent: 100 } : f
|
|
116
|
+
));
|
|
117
|
+
},
|
|
118
|
+
onError: (error: any) => {
|
|
119
|
+
setMergedFileList(prev => prev.map(f =>
|
|
120
|
+
f.uid === uid ? { ...f, status: 'error' as const, error } : f
|
|
121
|
+
));
|
|
122
|
+
},
|
|
123
|
+
onProgress: (event: any) => {
|
|
124
|
+
setMergedFileList(prev => prev.map(f =>
|
|
125
|
+
f.uid === uid ? { ...f, percent: event?.percent } : f
|
|
126
|
+
));
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{ defaultRequest: () => undefined },
|
|
130
|
+
);
|
|
53
131
|
};
|
|
54
132
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
uid,
|
|
58
|
-
name: fileName || `pasted-${Date.now()}.${getExtension()}`,
|
|
59
|
-
size: file.size,
|
|
60
|
-
type: fileType,
|
|
61
|
-
status: 'uploading',
|
|
62
|
-
percent: 0,
|
|
63
|
-
originFileObj: file as any,
|
|
64
|
-
};
|
|
133
|
+
try {
|
|
134
|
+
const beforeUploadResult = beforeUpload?.(file as any, [file as any]);
|
|
65
135
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const reader = new FileReader();
|
|
70
|
-
reader.onload = (e) => {
|
|
71
|
-
const dataUrl = e.target?.result;
|
|
72
|
-
if (typeof dataUrl === 'string') {
|
|
73
|
-
setFileList(prev => prev.map(f =>
|
|
74
|
-
f.uid === uid ? { ...f, thumbUrl: dataUrl } : f
|
|
75
|
-
));
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
reader.readAsDataURL(file);
|
|
79
|
-
}
|
|
136
|
+
if (beforeUploadResult === false || beforeUploadResult === Upload.LIST_IGNORE) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
80
139
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
{ defaultRequest: () => undefined },
|
|
104
|
-
);
|
|
105
|
-
}, [rest?.customRequest, rest?.accept]);
|
|
140
|
+
if (beforeUploadResult && typeof (beforeUploadResult as Promise<unknown>).then === 'function') {
|
|
141
|
+
(beforeUploadResult as Promise<File | Blob | boolean | string>).then((processedFile) => {
|
|
142
|
+
if (processedFile === false || processedFile === Upload.LIST_IGNORE) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (processedFile && typeof processedFile === 'object') {
|
|
147
|
+
startUpload(processedFile as File | Blob);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
startUpload(file);
|
|
152
|
+
}).catch((error) => {
|
|
153
|
+
console.error('beforeUpload promise rejected:', error);
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (beforeUploadResult && typeof beforeUploadResult === 'object') {
|
|
159
|
+
startUpload(beforeUploadResult as unknown as File | Blob);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
106
162
|
|
|
107
|
-
|
|
163
|
+
startUpload(file);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error('beforeUpload rejected:', error);
|
|
166
|
+
}
|
|
167
|
+
}, [accept, beforeUpload, customRequest, disabled, maxCount, setMergedFileList]);
|
|
168
|
+
|
|
169
|
+
if (customRequest) {
|
|
108
170
|
const uploadIconButton = <Upload
|
|
109
171
|
fileList={fileList}
|
|
110
172
|
showUploadList={false}
|
|
173
|
+
{...rest}
|
|
111
174
|
onChange={(info) => {
|
|
112
|
-
|
|
175
|
+
setMergedFileList(info.fileList);
|
|
176
|
+
onChange?.(info);
|
|
113
177
|
}}
|
|
114
|
-
{
|
|
115
|
-
disabled={options?.disabled}
|
|
178
|
+
disabled={disabled}
|
|
116
179
|
>
|
|
117
180
|
{
|
|
118
|
-
trigger ? React.createElement(trigger, { disabled
|
|
119
|
-
disabled={
|
|
181
|
+
trigger ? React.createElement(trigger, { disabled }) : <IconButton
|
|
182
|
+
disabled={disabled}
|
|
120
183
|
icon={<SparkAttachmentLine />}
|
|
121
184
|
bordered={false}
|
|
122
185
|
/>
|
|
@@ -130,7 +193,7 @@ export default function useAttachments(
|
|
|
130
193
|
>
|
|
131
194
|
<Attachments
|
|
132
195
|
items={fileList}
|
|
133
|
-
onChange={(info) =>
|
|
196
|
+
onChange={(info) => setMergedFileList(info.fileList)}
|
|
134
197
|
/>
|
|
135
198
|
</Sender.Header>
|
|
136
199
|
|
|
@@ -138,8 +201,9 @@ export default function useAttachments(
|
|
|
138
201
|
return {
|
|
139
202
|
fileList,
|
|
140
203
|
getFileList,
|
|
141
|
-
setFileList,
|
|
142
|
-
handlePasteFile,
|
|
204
|
+
setFileList: setMergedFileList,
|
|
205
|
+
handlePasteFile: handleManualFile,
|
|
206
|
+
handleDropFile: handleManualFile,
|
|
143
207
|
uploadIconButton,
|
|
144
208
|
uploadFileListHeader
|
|
145
209
|
}
|
|
@@ -147,7 +211,11 @@ export default function useAttachments(
|
|
|
147
211
|
} else {
|
|
148
212
|
return {
|
|
149
213
|
enabled: false,
|
|
214
|
+
fileList,
|
|
215
|
+
getFileList,
|
|
216
|
+
setFileList: setMergedFileList,
|
|
150
217
|
handlePasteFile: undefined,
|
|
218
|
+
handleDropFile: undefined,
|
|
151
219
|
};
|
|
152
220
|
}
|
|
153
|
-
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import type { IAgentScopeRuntimeWebUIMessage } from "../../../types/IMessages";
|
|
2
|
+
import {
|
|
3
|
+
AgentScopeRuntimeContentType,
|
|
4
|
+
IAgentScopeRuntimeRequest,
|
|
5
|
+
IContent,
|
|
6
|
+
} from "../../../AgentScopeRuntime/types";
|
|
7
|
+
import type { AttachmentPreview, UserMessageAnchor } from "./types";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_USER_MESSAGE_ANCHOR_MIN_COUNT = 3;
|
|
10
|
+
const DEFAULT_USER_MESSAGE_ANCHOR_MIN_GAP = 6;
|
|
11
|
+
const DEFAULT_USER_MESSAGE_ANCHOR_BADGE_MAX_COUNT = 99;
|
|
12
|
+
const SCROLL_BOUNDARY_OFFSET = 4;
|
|
13
|
+
const SCROLL_VISIBLE_TOP_GAP = 16;
|
|
14
|
+
|
|
15
|
+
function normalizePreviewText(text: string) {
|
|
16
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getFileSizeText(size?: number) {
|
|
20
|
+
if (!size) return '';
|
|
21
|
+
if (size < 1024) return `${size} B`;
|
|
22
|
+
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
|
23
|
+
return `${(size / 1024 / 1024).toFixed(1)} MB`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getAttachmentPreview(content: IContent): AttachmentPreview | undefined {
|
|
27
|
+
switch (content.type) {
|
|
28
|
+
case AgentScopeRuntimeContentType.IMAGE:
|
|
29
|
+
return { type: 'Image', name: 'Image attachment' };
|
|
30
|
+
case AgentScopeRuntimeContentType.VIDEO:
|
|
31
|
+
return { type: 'Video', name: 'Video attachment' };
|
|
32
|
+
case AgentScopeRuntimeContentType.AUDIO:
|
|
33
|
+
return { type: 'Audio', name: 'Audio attachment' };
|
|
34
|
+
case AgentScopeRuntimeContentType.FILE:
|
|
35
|
+
return {
|
|
36
|
+
type: 'File',
|
|
37
|
+
name: [
|
|
38
|
+
content.file_name || content.fileName || content.file_url || content.file_id || 'File attachment',
|
|
39
|
+
getFileSizeText(content.file_size),
|
|
40
|
+
].filter(Boolean).join(' · '),
|
|
41
|
+
};
|
|
42
|
+
case AgentScopeRuntimeContentType.DATA:
|
|
43
|
+
return { type: 'Data', name: 'Data payload' };
|
|
44
|
+
default:
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getContentText(content: IContent) {
|
|
50
|
+
if (content.type === AgentScopeRuntimeContentType.TEXT) return content.text;
|
|
51
|
+
if (content.type === AgentScopeRuntimeContentType.REFUSAL) return content.refusal;
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getUserMessageAnchor(message: IAgentScopeRuntimeWebUIMessage): UserMessageAnchor | undefined {
|
|
56
|
+
if (message.role !== 'user' || !message.id) return undefined;
|
|
57
|
+
|
|
58
|
+
const request = message.cards?.find((card) => card.code === 'AgentScopeRuntimeRequestCard')?.data as
|
|
59
|
+
| IAgentScopeRuntimeRequest
|
|
60
|
+
| undefined;
|
|
61
|
+
const contentList = request?.input?.flatMap((item) => item.content || []) || [];
|
|
62
|
+
const text = normalizePreviewText(contentList.map(getContentText).filter(Boolean).join(' '));
|
|
63
|
+
const attachments = contentList.map(getAttachmentPreview).filter(Boolean) as AttachmentPreview[];
|
|
64
|
+
const attachmentPreview = attachments.map((item) => item.name).join(' ');
|
|
65
|
+
const preview = normalizePreviewText(text || attachmentPreview || '');
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
createdAt: request?.created_at,
|
|
69
|
+
id: message.id,
|
|
70
|
+
attachments,
|
|
71
|
+
orderPercent: 0,
|
|
72
|
+
preview: preview || 'User message',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getAnchorTimeText(createdAt?: number) {
|
|
77
|
+
if (!createdAt) return '';
|
|
78
|
+
|
|
79
|
+
const timestamp = createdAt < 10000000000 ? createdAt * 1000 : createdAt;
|
|
80
|
+
const date = new Date(timestamp);
|
|
81
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
82
|
+
|
|
83
|
+
const month = date.getMonth() + 1;
|
|
84
|
+
const day = date.getDate();
|
|
85
|
+
const hour = String(date.getHours()).padStart(2, '0');
|
|
86
|
+
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
87
|
+
return `${month}月${day}日 ${hour}:${minute}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getUserMessageAnchorMinCount(minCount?: number) {
|
|
91
|
+
if (typeof minCount !== 'number' || !Number.isFinite(minCount)) {
|
|
92
|
+
return DEFAULT_USER_MESSAGE_ANCHOR_MIN_COUNT;
|
|
93
|
+
}
|
|
94
|
+
return Math.max(0, Math.floor(minCount));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getUserMessageAnchorMinGap(minGap?: number) {
|
|
98
|
+
if (typeof minGap !== 'number' || !Number.isFinite(minGap)) {
|
|
99
|
+
return DEFAULT_USER_MESSAGE_ANCHOR_MIN_GAP;
|
|
100
|
+
}
|
|
101
|
+
return Math.max(0, minGap);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getUserMessageAnchorBadgeMaxCount(badgeMaxCount?: number) {
|
|
105
|
+
if (typeof badgeMaxCount !== 'number' || !Number.isFinite(badgeMaxCount)) {
|
|
106
|
+
return DEFAULT_USER_MESSAGE_ANCHOR_BADGE_MAX_COUNT;
|
|
107
|
+
}
|
|
108
|
+
return Math.max(1, Math.floor(badgeMaxCount));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function getUserMessageAnchorBadgeText(count: number, badgeMaxCount?: number) {
|
|
112
|
+
const normalizedBadgeMaxCount = getUserMessageAnchorBadgeMaxCount(badgeMaxCount);
|
|
113
|
+
return count > normalizedBadgeMaxCount ? `${normalizedBadgeMaxCount}+` : String(count);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function clamp(value: number, min: number, max: number) {
|
|
117
|
+
return Math.min(Math.max(value, min), max);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function areAnchorPositionsEqual(prev: Record<string, number>, next: Record<string, number>) {
|
|
121
|
+
const prevKeys = Object.keys(prev);
|
|
122
|
+
const nextKeys = Object.keys(next);
|
|
123
|
+
if (prevKeys.length !== nextKeys.length) return false;
|
|
124
|
+
return nextKeys.every((key) => Math.abs((prev[key] ?? -1) - next[key]) < 0.1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getMessageElementMapInScrollContainer(scrollEl: HTMLElement) {
|
|
128
|
+
const candidates = scrollEl.querySelectorAll<HTMLElement>('[data-role][id]');
|
|
129
|
+
const result = new Map<string, HTMLElement>();
|
|
130
|
+
candidates.forEach((candidate) => {
|
|
131
|
+
result.set(candidate.id, candidate);
|
|
132
|
+
});
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getMessageElementInScrollContainer(scrollEl: HTMLElement, messageId: string) {
|
|
137
|
+
const target = typeof document === 'undefined' ? null : document.getElementById(messageId);
|
|
138
|
+
if (target instanceof HTMLElement && scrollEl.contains(target) && target.hasAttribute('data-role')) {
|
|
139
|
+
return target;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return getMessageElementMapInScrollContainer(scrollEl).get(messageId) || null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getScrollBoundaryAnchorId(scrollEl: HTMLElement, anchors: UserMessageAnchor[]) {
|
|
146
|
+
const maxScrollDistance = Math.max(scrollEl.scrollHeight - scrollEl.clientHeight, 0);
|
|
147
|
+
if (maxScrollDistance <= SCROLL_BOUNDARY_OFFSET) return undefined;
|
|
148
|
+
|
|
149
|
+
const firstAnchor = anchors[0];
|
|
150
|
+
const lastAnchor = anchors[anchors.length - 1];
|
|
151
|
+
if (!firstAnchor || !lastAnchor) return undefined;
|
|
152
|
+
|
|
153
|
+
const isDescOrder = scrollEl.className.includes('bubble-list-order-desc');
|
|
154
|
+
if (isDescOrder) {
|
|
155
|
+
const scrollDistance = Math.abs(scrollEl.scrollTop);
|
|
156
|
+
if (scrollDistance <= SCROLL_BOUNDARY_OFFSET) return lastAnchor.id;
|
|
157
|
+
if (maxScrollDistance - scrollDistance <= SCROLL_BOUNDARY_OFFSET) return firstAnchor.id;
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (scrollEl.scrollTop <= SCROLL_BOUNDARY_OFFSET) return firstAnchor.id;
|
|
162
|
+
if (maxScrollDistance - scrollEl.scrollTop <= SCROLL_BOUNDARY_OFFSET) return lastAnchor.id;
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function getScrollVisibleTopOffset(scrollEl: HTMLElement) {
|
|
167
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
168
|
+
const layoutRight = scrollEl.closest('[class*="chat-anywhere-layout-right"]') as HTMLElement | null;
|
|
169
|
+
const header = layoutRight?.querySelector('[class*="chat-anywhere-layout-right-header"]') as HTMLElement | null;
|
|
170
|
+
const headerRect = header?.getBoundingClientRect();
|
|
171
|
+
const headerOverlap = headerRect ? Math.max(0, headerRect.bottom - scrollRect.top) : 0;
|
|
172
|
+
const scrollPaddingTop = Number.parseFloat(window.getComputedStyle(scrollEl).scrollPaddingTop) || 0;
|
|
173
|
+
|
|
174
|
+
return Math.max(scrollPaddingTop, headerOverlap + (headerOverlap > 0 ? SCROLL_VISIBLE_TOP_GAP : 0));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getActiveAnchorId(scrollEl: HTMLElement, anchors: UserMessageAnchor[], topOffset = 0) {
|
|
178
|
+
const targetMap = getMessageElementMapInScrollContainer(scrollEl);
|
|
179
|
+
const renderedAnchors = anchors.filter((anchor) => targetMap.has(anchor.id));
|
|
180
|
+
const boundaryAnchorId = getScrollBoundaryAnchorId(scrollEl, renderedAnchors);
|
|
181
|
+
if (boundaryAnchorId) return boundaryAnchorId;
|
|
182
|
+
|
|
183
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
184
|
+
const activeLineTop = scrollRect.top + topOffset;
|
|
185
|
+
let activeAnchorId: string | undefined;
|
|
186
|
+
let minDistance = Number.POSITIVE_INFINITY;
|
|
187
|
+
|
|
188
|
+
renderedAnchors.forEach((anchor) => {
|
|
189
|
+
const target = targetMap.get(anchor.id);
|
|
190
|
+
if (!target) return;
|
|
191
|
+
|
|
192
|
+
const targetRect = target.getBoundingClientRect();
|
|
193
|
+
const distance = Math.abs(targetRect.top - activeLineTop);
|
|
194
|
+
|
|
195
|
+
if (distance < minDistance) {
|
|
196
|
+
minDistance = distance;
|
|
197
|
+
activeAnchorId = anchor.id;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return activeAnchorId;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getTargetTopOffset(scrollEl: HTMLElement, target: HTMLElement, topOffset = 0) {
|
|
205
|
+
const scrollRect = scrollEl.getBoundingClientRect();
|
|
206
|
+
const targetRect = target.getBoundingClientRect();
|
|
207
|
+
|
|
208
|
+
return targetRect.top - scrollRect.top - topOffset;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function scrollTargetIntoContainerTop(
|
|
212
|
+
scrollEl: HTMLElement,
|
|
213
|
+
target: HTMLElement,
|
|
214
|
+
behavior: ScrollBehavior = 'smooth',
|
|
215
|
+
topOffset = 0,
|
|
216
|
+
) {
|
|
217
|
+
const offset = getTargetTopOffset(scrollEl, target, topOffset);
|
|
218
|
+
if (Math.abs(offset) < 1) return;
|
|
219
|
+
|
|
220
|
+
scrollEl.scrollBy({
|
|
221
|
+
top: offset,
|
|
222
|
+
behavior,
|
|
223
|
+
});
|
|
224
|
+
}
|