@agentscope-ai/chat 1.1.71-beta.1782732003943 → 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.
Files changed (75) hide show
  1. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/index.tsx +16 -75
  2. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.tsx +150 -82
  3. package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/helpers.ts +224 -0
  4. package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/index.tsx +673 -0
  5. package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/types.ts +27 -0
  6. package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/index.tsx +114 -26
  7. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.tsx +122 -265
  8. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.tsx +6 -32
  9. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.tsx +9 -17
  10. package/components/AgentScopeRuntimeWebUI/core/Chat/index.tsx +3 -42
  11. package/components/AgentScopeRuntimeWebUI/core/Chat/styles.tsx +322 -254
  12. package/components/AgentScopeRuntimeWebUI/core/ChatAnywhere/index.tsx +1 -1
  13. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.tsx +8 -34
  14. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereInputContext.tsx +11 -4
  15. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereSessionsContext.tsx +15 -82
  16. package/components/AgentScopeRuntimeWebUI/core/Layout/styles.tsx +3 -3
  17. package/components/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.ts +66 -68
  18. package/components/AgentScopeRuntimeWebUI/core/types/ISessions.tsx +1 -5
  19. package/components/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.ts +5 -0
  20. package/components/AgentScopeRuntimeWebUI/starter/index.tsx +20 -99
  21. package/components/Sender/index.tsx +157 -26
  22. package/components/Sender/style/index.ts +13 -0
  23. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.d.ts +0 -8
  24. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.js +24 -104
  25. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.d.ts +6 -4
  26. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.js +153 -101
  27. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/helpers.d.ts +16 -0
  28. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/helpers.js +200 -0
  29. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/index.d.ts +2 -0
  30. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/index.js +743 -0
  31. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/types.d.ts +24 -0
  32. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/UserMessageAnchors/types.js +1 -0
  33. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/index.js +154 -28
  34. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.d.ts +1 -15
  35. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.js +147 -259
  36. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.d.ts +1 -1
  37. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.js +65 -98
  38. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.d.ts +3 -3
  39. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.js +12 -33
  40. package/lib/AgentScopeRuntimeWebUI/core/Chat/index.js +2 -33
  41. package/lib/AgentScopeRuntimeWebUI/core/Chat/styles.js +52 -44
  42. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.d.ts +1 -23
  43. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.js +8 -32
  44. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereInputContext.js +8 -4
  45. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereSessionsContext.js +21 -110
  46. package/lib/AgentScopeRuntimeWebUI/core/Layout/styles.js +1 -1
  47. package/lib/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.d.ts +60 -79
  48. package/lib/AgentScopeRuntimeWebUI/core/types/ISessions.d.ts +0 -4
  49. package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.d.ts +5 -0
  50. package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.js +5 -0
  51. package/lib/AgentScopeRuntimeWebUI/starter/index.js +26 -144
  52. package/lib/Sender/index.d.ts +29 -5
  53. package/lib/Sender/index.js +116 -21
  54. package/lib/Sender/style/index.js +11 -1
  55. package/package.json +1 -2
  56. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.tsx +0 -289
  57. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.scenarios.zh-CN.md +0 -51
  58. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.test.ts +0 -758
  59. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.ts +0 -421
  60. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.ts +0 -375
  61. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.ts +0 -102
  62. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.ts +0 -87
  63. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.tsx +0 -1129
  64. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.d.ts +0 -15
  65. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.js +0 -275
  66. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.d.ts +0 -10
  67. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.js +0 -586
  68. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.d.ts +0 -90
  69. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.js +0 -242
  70. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.d.ts +0 -17
  71. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.js +0 -39
  72. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.d.ts +0 -5
  73. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.js +0 -108
  74. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.d.ts +0 -59
  75. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.js +0 -965
@@ -1,21 +1,14 @@
1
- import { useCallback, type ReactNode } from "react";
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 data = getSubmittableData();
85
- if (!data) return;
86
-
87
- if (props.queue && (
88
- inputContext.loading ||
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
- }, [props.onCancel]);
58
+ }, []);
120
59
 
121
- return <div className={prefixCls} onKeyDownCapture={handleKeyDownCapture}>
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 handlePasteFile = useCallback((file: File) => {
25
- if (!rest?.customRequest) return;
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
- if (rest.accept) {
31
- const matched = rest.accept.split(',').some(pattern => {
32
- const trimmed = pattern.trim();
33
- if (!trimmed) return false;
34
- if (trimmed.startsWith('.')) return fileName.toLowerCase().endsWith(trimmed.toLowerCase());
35
- if (trimmed === '*/*') return true;
36
- if (trimmed.includes('*')) {
37
- const [acceptMain] = trimmed.split('/');
38
- const [fileMain] = fileType.split('/');
39
- return acceptMain === fileMain;
40
- }
41
- return fileType === trimmed;
42
- });
43
- if (!matched) return;
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
- if ((rest as any).maxCount && fileListRef.current.length >= (rest as any).maxCount) return;
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
- const getExtension = () => {
49
- const nameMatch = fileName.match(/\.([^.]+)$/);
50
- if (nameMatch) return nameMatch[1].toLowerCase();
51
- const typeMatch = fileType.match(/\/([^/+]+)/);
52
- return typeMatch ? typeMatch[1].toLowerCase() : 'bin';
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
- const uid = `paste-${Date.now()}-${uidCounter.current++}`;
56
- const uploadFile: UploadFile = {
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
- setFileList(prev => [...prev, uploadFile]);
67
-
68
- if (fileType.startsWith('image/')) {
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
- rest.customRequest(
82
- {
83
- file,
84
- filename: 'file',
85
- action: '',
86
- method: 'POST',
87
- onSuccess: (response: any) => {
88
- setFileList(prev => prev.map(f =>
89
- f.uid === uid ? { ...f, status: 'done' as const, response, percent: 100 } : f
90
- ));
91
- },
92
- onError: (error: any) => {
93
- setFileList(prev => prev.map(f =>
94
- f.uid === uid ? { ...f, status: 'error' as const, error } : f
95
- ));
96
- },
97
- onProgress: (event: any) => {
98
- setFileList(prev => prev.map(f =>
99
- f.uid === uid ? { ...f, percent: event?.percent } : f
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
- if (rest?.customRequest) {
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
- setFileList(info.fileList);
175
+ setMergedFileList(info.fileList);
176
+ onChange?.(info);
113
177
  }}
114
- {...rest}
115
- disabled={options?.disabled}
178
+ disabled={disabled}
116
179
  >
117
180
  {
118
- trigger ? React.createElement(trigger, { disabled: options?.disabled }) : <IconButton
119
- disabled={options?.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) => setFileList(info.fileList)}
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
+ }