@agentscope-ai/chat 1.1.72-beta.1785916409537 → 1.1.72

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 (104) hide show
  1. package/components/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Actions.tsx +2 -5
  2. package/components/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Card.tsx +1 -4
  3. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/index.tsx +114 -125
  4. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/mentionUtils.ts +104 -0
  5. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.tsx +105 -54
  6. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/useLongTextUpload.ts +196 -0
  7. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/useMentions.tsx +442 -0
  8. package/components/AgentScopeRuntimeWebUI/core/Chat/MessageList/index.tsx +4 -4
  9. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.tsx +128 -359
  10. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatMessageHandler.tsx +60 -61
  11. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.tsx +42 -70
  12. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.tsx +9 -17
  13. package/components/AgentScopeRuntimeWebUI/core/Chat/index.tsx +3 -49
  14. package/components/AgentScopeRuntimeWebUI/core/Chat/styles.tsx +104 -303
  15. package/components/AgentScopeRuntimeWebUI/core/ChatAnywhere/index.tsx +1 -1
  16. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.tsx +8 -36
  17. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereInputContext.tsx +11 -4
  18. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereOptionsContext.tsx +2 -13
  19. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereSessionsContext.tsx +15 -118
  20. package/components/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.ts +161 -219
  21. package/components/AgentScopeRuntimeWebUI/core/types/ISessions.tsx +1 -5
  22. package/components/AgentScopeRuntimeWebUI/index.tsx +3 -15
  23. package/components/AgentScopeRuntimeWebUI/starter/OptionsPanel/OptionsEditor.tsx +11 -4
  24. package/components/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.ts +3 -0
  25. package/components/AgentScopeRuntimeWebUI/starter/index.tsx +30 -110
  26. package/components/Bubble/BubbleList.tsx +6 -1
  27. package/components/Sender/index.tsx +256 -168
  28. package/components/index.ts +5 -8
  29. package/lib/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Actions.js +4 -6
  30. package/lib/AgentScopeRuntimeWebUI/core/AgentScopeRuntime/Response/Card.js +2 -3
  31. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.d.ts +1 -9
  32. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.js +96 -132
  33. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/mentionUtils.d.ts +15 -0
  34. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/mentionUtils.js +55 -0
  35. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.d.ts +12 -2
  36. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useAttachments.js +115 -64
  37. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useLongTextUpload.d.ts +22 -0
  38. package/lib/AgentScopeRuntimeWebUI/core/Chat/{submission.js → Input/useLongTextUpload.js} +148 -97
  39. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useMentions.d.ts +14 -0
  40. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/useMentions.js +378 -0
  41. package/lib/AgentScopeRuntimeWebUI/core/Chat/MessageList/index.js +4 -4
  42. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.d.ts +2 -16
  43. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.js +211 -390
  44. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatMessageHandler.d.ts +1 -1
  45. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatMessageHandler.js +17 -6
  46. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.d.ts +1 -5
  47. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatRequest.js +113 -152
  48. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.d.ts +3 -3
  49. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatSessionHandler.js +12 -33
  50. package/lib/AgentScopeRuntimeWebUI/core/Chat/index.js +2 -41
  51. package/lib/AgentScopeRuntimeWebUI/core/Chat/styles.js +8 -128
  52. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.d.ts +1 -25
  53. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.js +8 -34
  54. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereInputContext.js +8 -4
  55. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereOptionsContext.js +2 -13
  56. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereSessionsContext.js +21 -131
  57. package/lib/AgentScopeRuntimeWebUI/core/types/IChatAnywhere.d.ts +148 -208
  58. package/lib/AgentScopeRuntimeWebUI/core/types/ISessions.d.ts +0 -4
  59. package/lib/AgentScopeRuntimeWebUI/index.d.ts +3 -4
  60. package/lib/AgentScopeRuntimeWebUI/index.js +3 -4
  61. package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/OptionsEditor.js +9 -2
  62. package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.d.ts +3 -0
  63. package/lib/AgentScopeRuntimeWebUI/starter/OptionsPanel/defaultConfig.js +3 -0
  64. package/lib/AgentScopeRuntimeWebUI/starter/index.js +37 -157
  65. package/lib/Bubble/BubbleList.js +13 -7
  66. package/lib/Sender/index.d.ts +20 -9
  67. package/lib/Sender/index.js +38 -25
  68. package/lib/index.d.ts +2 -1
  69. package/lib/index.js +2 -1
  70. package/package.json +1 -2
  71. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/submission.ts +0 -11
  72. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.tsx +0 -371
  73. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.scenarios.zh-CN.md +0 -59
  74. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.test.ts +0 -1272
  75. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/actions.ts +0 -60
  76. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.ts +0 -534
  77. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.ts +0 -613
  78. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.ts +0 -102
  79. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.ts +0 -220
  80. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/submission.ts +0 -39
  81. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.tsx +0 -1672
  82. package/components/AgentScopeRuntimeWebUI/core/Chat/submission.ts +0 -140
  83. package/components/AgentScopeRuntimeWebUI/core/Context/sessionIdentity.ts +0 -78
  84. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/submission.d.ts +0 -7
  85. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/submission.js +0 -6
  86. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.d.ts +0 -17
  87. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.js +0 -346
  88. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/actions.d.ts +0 -9
  89. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/actions.js +0 -23
  90. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.d.ts +0 -23
  91. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/background.js +0 -722
  92. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.d.ts +0 -111
  93. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.js +0 -378
  94. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.d.ts +0 -17
  95. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/session.js +0 -39
  96. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.d.ts +0 -8
  97. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/storage.js +0 -274
  98. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/submission.d.ts +0 -3
  99. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/submission.js +0 -25
  100. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.d.ts +0 -67
  101. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useInputQueueController.js +0 -1391
  102. package/lib/AgentScopeRuntimeWebUI/core/Chat/submission.d.ts +0 -22
  103. package/lib/AgentScopeRuntimeWebUI/core/Context/sessionIdentity.d.ts +0 -15
  104. package/lib/AgentScopeRuntimeWebUI/core/Context/sessionIdentity.js +0 -78
@@ -26,7 +26,7 @@ export default function Tools(props: {
26
26
  isLast?: boolean;
27
27
  }) {
28
28
  const { t } = useTranslation();
29
- const defaultActions = [
29
+ const actionsOptionsList = useChatAnywhereOptions(v => v.actions?.list) || [
30
30
  {
31
31
  icon: <SparkCopyLine />,
32
32
  onClick: () => {
@@ -34,10 +34,6 @@ export default function Tools(props: {
34
34
  }
35
35
  }
36
36
  ];
37
- const configuredActions = useChatAnywhereOptions(v => v.actions?.list);
38
- const actionsOptionsList = Array.isArray(configuredActions)
39
- ? configuredActions
40
- : defaultActions;
41
37
 
42
38
  const replace = useChatAnywhereOptions(v => v.actions?.replace) ?? true;
43
39
  const rightOption = useChatAnywhereOptions(v => v.actions?.right);
@@ -88,3 +84,4 @@ export default function Tools(props: {
88
84
  right={rightNode}
89
85
  />
90
86
  }
87
+
@@ -33,9 +33,6 @@ function DefaultResponseRender(props: {
33
33
  }) {
34
34
  const avatar = useChatAnywhereOptions(v => v.welcome.avatar);
35
35
  const nick = useChatAnywhereOptions(v => v.welcome.nick);
36
- const nickNode = typeof nick === 'string' || React.isValidElement(nick)
37
- ? nick
38
- : null;
39
36
  const messages = useMemo(() => {
40
37
  return AgentScopeRuntimeResponseBuilder.mergeToolMessages(props.data.output);
41
38
  }, [props.data.output])
@@ -46,7 +43,7 @@ function DefaultResponseRender(props: {
46
43
  return <>
47
44
  {avatar && <Flex align="center" gap={8} style={{ marginBottom: 8 }}>
48
45
  <Avatar src={avatar} />
49
- {nickNode && <span>{nickNode}</span>}
46
+ {nick && <span>{nick as string}</span>}
50
47
  </Flex>}
51
48
  {props.contentPrepend ?? null}
52
49
  {
@@ -5,26 +5,16 @@ import {
5
5
  useProviderContext,
6
6
  } from '@agentscope-ai/chat';
7
7
  import { useGetState } from 'ahooks';
8
- import { useCallback, type ReactNode } from 'react';
8
+ import { Children, useCallback, useRef } from 'react';
9
9
  import { useChatAnywhereInput } from '../../Context/ChatAnywhereInputContext';
10
10
  import { useChatAnywhereOptions } from '../../Context/ChatAnywhereOptionsContext';
11
- import type { QueueEnqueueResult, QueuedInputItem } from '../InputQueue';
12
- import { hasSubmittableAttachments } from './submission';
13
- import useAttachments from './useAttachments';
11
+ import useAttachments, { type AttachmentUploadFile } from './useAttachments';
12
+ import useLongTextUpload from './useLongTextUpload';
13
+ import useMentions from './useMentions';
14
14
 
15
15
  export interface InputProps {
16
16
  onCancel: () => void;
17
- onSubmit: (
18
- data: IAgentScopeRuntimeWebUIInputData,
19
- ) => void | QueueEnqueueResult | Promise<void | QueueEnqueueResult>;
20
- queue?: {
21
- items: QueuedInputItem[];
22
- isOwner: boolean;
23
- panel?: ReactNode;
24
- onEnqueue: (
25
- data: IAgentScopeRuntimeWebUIInputData,
26
- ) => QueueEnqueueResult | Promise<QueueEnqueueResult>;
27
- };
17
+ onSubmit: (data: IAgentScopeRuntimeWebUIInputData) => void;
28
18
  }
29
19
 
30
20
  export default function Input(props: InputProps) {
@@ -32,6 +22,7 @@ export default function Input(props: InputProps) {
32
22
  const prefixCls = useProviderContext().getPrefixCls('chat-anywhere-input');
33
23
  const senderOptions = useChatAnywhereOptions((v) => v.sender);
34
24
  const inputContext = useChatAnywhereInput((v) => v);
25
+ const uploadFileRef = useRef<AttachmentUploadFile | null>(null);
35
26
 
36
27
  const {
37
28
  placeholder = '',
@@ -47,142 +38,140 @@ export default function Input(props: InputProps) {
47
38
  prefix,
48
39
  allowSpeech,
49
40
  suggestions,
41
+ mentions,
42
+ components,
43
+ longTextUpload,
50
44
  } = senderOptions || {};
51
45
 
46
+ const mentionController = useMentions(
47
+ mentions,
48
+ content,
49
+ setContent,
50
+ !!inputContext.disabled,
51
+ );
52
+
53
+ const uploadLongTextFile = useCallback<AttachmentUploadFile>(
54
+ (fileToUpload, uploadOptions) => {
55
+ if (!uploadFileRef.current) {
56
+ return Promise.reject(new Error('Upload request is not available.'));
57
+ }
58
+
59
+ return uploadFileRef.current(fileToUpload, uploadOptions);
60
+ },
61
+ [],
62
+ );
63
+
64
+ const longTextUploadController = useLongTextUpload({
65
+ options: longTextUpload,
66
+ fallbackCustomRequest: attachments?.customRequest,
67
+ maxLength,
68
+ getContent,
69
+ setContent,
70
+ clearMentions: mentionController.clear,
71
+ uploadFile: uploadLongTextFile,
72
+ onContentChange: mentionController.handleValueChange,
73
+ });
74
+
52
75
  const {
53
- fileList,
54
76
  getFileList,
55
77
  setFileList,
78
+ uploadFile,
56
79
  handlePasteFile,
57
80
  handleDropFile,
58
81
  uploadIconButton,
59
82
  uploadFileListHeader,
60
- } = useAttachments(attachments, { disabled: !!inputContext.disabled });
83
+ } = useAttachments(attachments, {
84
+ disabled: !!inputContext.disabled || longTextUploadController.uploading,
85
+ });
86
+ uploadFileRef.current = uploadFile;
61
87
 
62
- const getSubmittableData = useCallback(() => {
63
- const fileList = (getFileList?.() || []).filter((i) => i.response?.url);
64
- const query = getContent();
65
- if (!query.trim() && fileList.length === 0) return;
88
+ const handleSubmit = useCallback(async () => {
89
+ if (longTextUploadController.isUploading()) return;
66
90
 
67
- return {
68
- query,
69
- text: query,
91
+ const fileList = (getFileList?.() || []).filter((i) => i.response?.url);
92
+ const inputData: IAgentScopeRuntimeWebUIInputData = {
93
+ query: mentionController.getQuery(getContent()),
70
94
  fileList,
71
- attachments: fileList,
95
+ mentions: mentionController.mentions.map(({ value, type }) => ({
96
+ value,
97
+ type,
98
+ })),
72
99
  };
73
- }, [getContent, getFileList]);
100
+ const beforeSubmitResult = await beforeSubmit(inputData);
101
+ const normalizedResult =
102
+ typeof beforeSubmitResult === 'boolean'
103
+ ? { proceed: beforeSubmitResult }
104
+ : beforeSubmitResult;
105
+
106
+ if (normalizedResult.clear) {
107
+ longTextUploadController.resetPromptState();
108
+ setContent('');
109
+ setFileList?.([]);
110
+ mentionController.clear();
111
+ }
112
+ if (!normalizedResult.proceed) return;
74
113
 
75
- const clearInput = useCallback(() => {
114
+ props.onSubmit({
115
+ ...inputData,
116
+ query: normalizedResult.query ?? inputData.query,
117
+ });
118
+ longTextUploadController.resetPromptState();
76
119
  setContent('');
77
120
  setFileList?.([]);
78
- }, [setContent, setFileList]);
79
-
80
- const handleEnqueue = useCallback(async () => {
81
- const next = await beforeSubmit();
82
- if (!next) return;
83
-
84
- const data = getSubmittableData();
85
- if (!data) return;
86
- if (!props.queue) return;
87
-
88
- const result = await props.queue.onEnqueue(data);
89
- if (result.ok) {
90
- clearInput();
91
- }
92
- }, [beforeSubmit, clearInput, getSubmittableData, props.queue]);
93
-
94
- const handleSubmit = useCallback(async () => {
95
- const next = await beforeSubmit();
96
- if (!next) return;
97
-
98
- const data = getSubmittableData();
99
- if (!data) return;
100
-
101
- if (
102
- props.queue &&
103
- (inputContext.loading ||
104
- props.queue.items.length ||
105
- props.queue.isOwner === false)
106
- ) {
107
- const result = await props.queue.onEnqueue(data);
108
- if (result.ok) {
109
- clearInput();
110
- }
111
- } else {
112
- const result = await props.onSubmit(data);
113
- if (result && !result.ok) return;
114
- clearInput();
115
- }
121
+ mentionController.clear();
116
122
  }, [
117
123
  beforeSubmit,
118
- clearInput,
119
- getSubmittableData,
120
- inputContext.loading,
121
- props.onSubmit,
122
- props.queue,
124
+ getContent,
125
+ getFileList,
126
+ longTextUploadController,
127
+ mentionController,
128
+ props,
129
+ setContent,
130
+ setFileList,
123
131
  ]);
124
132
 
125
- const handleKeyDownCapture = useCallback(
126
- (event: React.KeyboardEvent<HTMLDivElement>) => {
127
- if (event.key !== 'Enter' || event.shiftKey) return;
128
- if (event.nativeEvent?.isComposing) return;
129
- if (!props.queue) return;
130
- const forceEnqueue = event.ctrlKey || event.metaKey;
131
- if (!forceEnqueue && !inputContext.loading && !props.queue?.items.length)
132
- return;
133
-
134
- const data = getSubmittableData();
135
- if (!data) return;
136
-
137
- event.preventDefault();
138
- event.stopPropagation();
139
- void (forceEnqueue ? handleEnqueue() : handleSubmit());
140
- },
141
- [
142
- getSubmittableData,
143
- handleEnqueue,
144
- handleSubmit,
145
- inputContext.loading,
146
- props.queue,
147
- props.queue?.items.length,
148
- ],
149
- );
150
-
151
133
  const handleCancel = useCallback(() => {
152
134
  props.onCancel();
153
- }, [props.onCancel]);
135
+ }, []);
154
136
 
155
137
  return (
156
- <div className={prefixCls} onKeyDownCapture={handleKeyDownCapture}>
138
+ <div className={prefixCls}>
157
139
  <div className={`${prefixCls}-wrapper`}>
158
140
  {beforeUI}
159
- {props.queue?.panel}
160
- <ChatInput
161
- loading={inputContext.loading}
162
- disabled={inputContext.disabled}
163
- placeholder={placeholder}
164
- value={content}
165
- prefix={
166
- <>
167
- {uploadIconButton}
168
- {prefix}
169
- </>
170
- }
171
- header={uploadFileListHeader}
172
- onChange={setContent}
173
- maxLength={maxLength}
174
- showCharacterCount={showCharacterCount}
175
- characterCountRender={characterCountRender}
176
- actionAffix={actionAffix}
177
- onSubmit={handleSubmit}
178
- onCancel={handleCancel}
179
- allowEmptySubmit={hasSubmittableAttachments(fileList)}
180
- allowSubmitWhenLoading={!!props.queue}
181
- allowSpeech={allowSpeech}
182
- onPasteFile={handlePasteFile}
183
- onDropFile={handleDropFile}
184
- suggestions={suggestions}
185
- />
141
+ {mentionController.wrapInput(
142
+ <ChatInput
143
+ loading={inputContext.loading}
144
+ disabled={
145
+ inputContext.disabled || longTextUploadController.uploading
146
+ }
147
+ placeholder={placeholder}
148
+ value={content}
149
+ prefix={Children.toArray([uploadIconButton, prefix])}
150
+ header={Children.toArray([
151
+ uploadFileListHeader,
152
+ mentionController.header,
153
+ ])}
154
+ onChange={longTextUploadController.handleContentChange}
155
+ onKeyDown={mentionController.handleKeyDown}
156
+ onSelectionChange={mentionController.handleSelectionChange}
157
+ onBlur={mentionController.close}
158
+ submitType={mentionController.open ? false : 'enter'}
159
+ allowEmptySubmit={mentionController.mentions.length > 0}
160
+ maxLength={maxLength}
161
+ truncateOnMaxLength={!longTextUploadController.enabled}
162
+ showCharacterCount={showCharacterCount}
163
+ characterCountRender={characterCountRender}
164
+ actionAffix={actionAffix}
165
+ onSubmit={handleSubmit}
166
+ onCancel={handleCancel}
167
+ allowSpeech={allowSpeech}
168
+ onPaste={longTextUploadController.handlePaste}
169
+ onPasteFile={handlePasteFile}
170
+ onDropFile={handleDropFile}
171
+ suggestions={suggestions}
172
+ components={components}
173
+ />,
174
+ )}
186
175
  {afterUI}
187
176
  </div>
188
177
  {disclaimer ? (
@@ -0,0 +1,104 @@
1
+ import type { IAgentScopeRuntimeWebUISenderMentionItem } from '@agentscope-ai/chat';
2
+
3
+ export interface MentionRange {
4
+ start: number;
5
+ end: number;
6
+ keyword: string;
7
+ }
8
+
9
+ export interface SelectedMention {
10
+ id: number;
11
+ item: IAgentScopeRuntimeWebUISenderMentionItem;
12
+ text: string;
13
+ start: number;
14
+ end: number;
15
+ }
16
+
17
+ function escapeRegExp(value: string) {
18
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
19
+ }
20
+
21
+ export function findMentionRange(
22
+ value: string,
23
+ cursor: number,
24
+ trigger: string,
25
+ ): MentionRange | null {
26
+ if (!trigger) return null;
27
+
28
+ const contentBeforeCursor = value.slice(0, cursor);
29
+ const match = contentBeforeCursor.match(
30
+ new RegExp(
31
+ `(?:^|\\s)${escapeRegExp(trigger)}([^\\s${escapeRegExp(trigger)}]*)$`,
32
+ ),
33
+ );
34
+ if (!match) return null;
35
+
36
+ const tokenLength = trigger.length + match[1].length;
37
+ let end = cursor;
38
+ while (
39
+ end < value.length &&
40
+ !/\s/.test(value[end]) &&
41
+ !value.startsWith(trigger, end)
42
+ ) {
43
+ end += 1;
44
+ }
45
+ return {
46
+ start: cursor - tokenLength,
47
+ end,
48
+ keyword: match[1],
49
+ };
50
+ }
51
+
52
+ export function remapSelectedMentions(
53
+ previousValue: string,
54
+ nextValue: string,
55
+ mentions: SelectedMention[],
56
+ ) {
57
+ if (previousValue === nextValue) return mentions;
58
+
59
+ let prefixLength = 0;
60
+ const maxPrefix = Math.min(previousValue.length, nextValue.length);
61
+ while (
62
+ prefixLength < maxPrefix &&
63
+ previousValue[prefixLength] === nextValue[prefixLength]
64
+ ) {
65
+ prefixLength += 1;
66
+ }
67
+
68
+ let suffixLength = 0;
69
+ const maxSuffix = Math.min(
70
+ previousValue.length - prefixLength,
71
+ nextValue.length - prefixLength,
72
+ );
73
+ while (
74
+ suffixLength < maxSuffix &&
75
+ previousValue[previousValue.length - 1 - suffixLength] ===
76
+ nextValue[nextValue.length - 1 - suffixLength]
77
+ ) {
78
+ suffixLength += 1;
79
+ }
80
+
81
+ const previousEditEnd = previousValue.length - suffixLength;
82
+ const nextEditEnd = nextValue.length - suffixLength;
83
+ const delta = nextEditEnd - previousEditEnd;
84
+
85
+ return mentions.flatMap((mention) => {
86
+ let nextMention = mention;
87
+ if (mention.end <= prefixLength) {
88
+ nextMention = mention;
89
+ } else if (mention.start >= previousEditEnd) {
90
+ nextMention = {
91
+ ...mention,
92
+ start: mention.start + delta,
93
+ end: mention.end + delta,
94
+ };
95
+ } else {
96
+ return [];
97
+ }
98
+
99
+ return nextValue.slice(nextMention.start, nextMention.end) ===
100
+ nextMention.text
101
+ ? [nextMention]
102
+ : [];
103
+ });
104
+ }
@@ -6,6 +6,19 @@ 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
+ type UploadCustomRequest = NonNullable<IAgentScopeRuntimeWebUISenderAttachmentsOptions['customRequest']>;
10
+
11
+ export interface UploadFileOptions {
12
+ customRequest?: UploadCustomRequest;
13
+ fileName?: string;
14
+ fileType?: string;
15
+ }
16
+
17
+ export type AttachmentUploadFile = (
18
+ fileToUpload: File | Blob,
19
+ uploadOptions?: UploadFileOptions,
20
+ ) => Promise<UploadFile>;
21
+
9
22
  function isAcceptedFile(file: File, accept?: string) {
10
23
  if (!accept) return true;
11
24
 
@@ -35,7 +48,7 @@ function getFileExtension(fileName: string, fileType: string) {
35
48
  }
36
49
 
37
50
  export default function useAttachments(
38
- attachments: IAgentScopeRuntimeWebUISenderAttachmentsOptions,
51
+ attachments?: IAgentScopeRuntimeWebUISenderAttachmentsOptions,
39
52
  options?: {
40
53
  disabled?: boolean;
41
54
  }
@@ -66,22 +79,29 @@ export default function useAttachments(
66
79
  const disabled = !!options?.disabled;
67
80
  const uidCounter = useRef(0);
68
81
 
69
- const handleManualFile = useCallback((file: File) => {
70
- if (disabled || !customRequest || !isAcceptedFile(file, accept)) return;
71
- if (maxCount && fileListRef.current.length >= maxCount) return;
72
-
73
- const fileType = file.type || '';
74
- const fileName = file.name || '';
82
+ const uploadFile = useCallback((
83
+ fileToUpload: File | Blob,
84
+ uploadOptions: UploadFileOptions = {},
85
+ ) => {
86
+ return new Promise<UploadFile>((resolve, reject) => {
87
+ const request = uploadOptions.customRequest || customRequest;
88
+ if (disabled || !request) {
89
+ reject(new Error('Upload request is not available.'));
90
+ return;
91
+ }
75
92
 
76
- const startUpload = (fileToUpload: File | Blob) => {
77
- if (maxCount && fileListRef.current.length >= maxCount) return;
93
+ if (maxCount && fileListRef.current.length >= maxCount) {
94
+ reject(new Error('Maximum upload count reached.'));
95
+ return;
96
+ }
78
97
 
79
- const uploadType = fileToUpload.type || fileType;
98
+ const uploadType = fileToUpload.type || uploadOptions.fileType || '';
99
+ const sourceName = uploadOptions.fileName || ('name' in fileToUpload ? fileToUpload.name : '');
80
100
  const timestamp = Date.now();
81
101
  const uid = `manual-${timestamp}-${uidCounter.current++}`;
82
- const uploadFile: UploadFile = {
102
+ const uploadFileItem: UploadFile = {
83
103
  uid,
84
- name: fileName || `file-${timestamp}.${getFileExtension(fileName, uploadType)}`,
104
+ name: sourceName || `file-${timestamp}.${getFileExtension(sourceName, uploadType)}`,
85
105
  size: fileToUpload.size,
86
106
  type: uploadType,
87
107
  status: 'uploading',
@@ -89,7 +109,7 @@ export default function useAttachments(
89
109
  originFileObj: fileToUpload as any,
90
110
  };
91
111
 
92
- setMergedFileList(prev => [...prev, uploadFile]);
112
+ setMergedFileList(prev => [...prev, uploadFileItem]);
93
113
 
94
114
  if (uploadType.startsWith('image/')) {
95
115
  const reader = new FileReader();
@@ -104,31 +124,45 @@ export default function useAttachments(
104
124
  reader.readAsDataURL(fileToUpload);
105
125
  }
106
126
 
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
- ));
127
+ try {
128
+ request(
129
+ {
130
+ file: fileToUpload as any,
131
+ filename: 'file',
132
+ action: '',
133
+ method: 'POST',
134
+ onSuccess: (response: any) => {
135
+ let completedFile: UploadFile | undefined;
136
+ setMergedFileList(prev => prev.map(f =>
137
+ f.uid === uid ? (completedFile = { ...f, status: 'done' as const, response, percent: 100 }) : f
138
+ ));
139
+ resolve(completedFile || { ...uploadFileItem, status: 'done', response, percent: 100 });
140
+ },
141
+ onError: (error: any) => {
142
+ setMergedFileList(prev => prev.map(f =>
143
+ f.uid === uid ? { ...f, status: 'error' as const, error } : f
144
+ ));
145
+ reject(error);
146
+ },
147
+ onProgress: (event: any) => {
148
+ setMergedFileList(prev => prev.map(f =>
149
+ f.uid === uid ? { ...f, percent: event?.percent } : f
150
+ ));
151
+ },
122
152
  },
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
- );
131
- };
153
+ { defaultRequest: () => undefined },
154
+ );
155
+ } catch (error) {
156
+ setMergedFileList(prev => prev.map(f =>
157
+ f.uid === uid ? { ...f, status: 'error' as const, error } : f
158
+ ));
159
+ reject(error);
160
+ }
161
+ });
162
+ }, [customRequest, disabled, maxCount, setMergedFileList]);
163
+
164
+ const handleManualFile = useCallback((file: File) => {
165
+ if (disabled || !customRequest || !isAcceptedFile(file, accept)) return;
132
166
 
133
167
  try {
134
168
  const beforeUploadResult = beforeUpload?.(file as any, [file as any]);
@@ -144,11 +178,19 @@ export default function useAttachments(
144
178
  }
145
179
 
146
180
  if (processedFile && typeof processedFile === 'object') {
147
- startUpload(processedFile as File | Blob);
181
+ uploadFile(processedFile as File | Blob, {
182
+ customRequest,
183
+ fileName: file.name,
184
+ fileType: file.type,
185
+ }).catch((error) => {
186
+ console.error('upload file rejected:', error);
187
+ });
148
188
  return;
149
189
  }
150
190
 
151
- startUpload(file);
191
+ uploadFile(file, { customRequest }).catch((error) => {
192
+ console.error('upload file rejected:', error);
193
+ });
152
194
  }).catch((error) => {
153
195
  console.error('beforeUpload promise rejected:', error);
154
196
  });
@@ -156,15 +198,33 @@ export default function useAttachments(
156
198
  }
157
199
 
158
200
  if (beforeUploadResult && typeof beforeUploadResult === 'object') {
159
- startUpload(beforeUploadResult as unknown as File | Blob);
201
+ uploadFile(beforeUploadResult as unknown as File | Blob, {
202
+ customRequest,
203
+ fileName: file.name,
204
+ fileType: file.type,
205
+ }).catch((error) => {
206
+ console.error('upload file rejected:', error);
207
+ });
160
208
  return;
161
209
  }
162
210
 
163
- startUpload(file);
211
+ uploadFile(file, { customRequest }).catch((error) => {
212
+ console.error('upload file rejected:', error);
213
+ });
164
214
  } catch (error) {
165
215
  console.error('beforeUpload rejected:', error);
166
216
  }
167
- }, [accept, beforeUpload, customRequest, disabled, maxCount, setMergedFileList]);
217
+ }, [accept, beforeUpload, customRequest, disabled, uploadFile]);
218
+
219
+ const uploadFileListHeader = <Sender.Header
220
+ closable={false}
221
+ open={fileList?.length > 0}
222
+ >
223
+ <Attachments
224
+ items={fileList}
225
+ onChange={(info) => setMergedFileList(info.fileList)}
226
+ />
227
+ </Sender.Header>
168
228
 
169
229
  if (customRequest) {
170
230
  const uploadIconButton = <Upload
@@ -186,22 +246,11 @@ export default function useAttachments(
186
246
  }
187
247
  </Upload>
188
248
 
189
-
190
- const uploadFileListHeader = <Sender.Header
191
- closable={false}
192
- open={fileList?.length > 0}
193
- >
194
- <Attachments
195
- items={fileList}
196
- onChange={(info) => setMergedFileList(info.fileList)}
197
- />
198
- </Sender.Header>
199
-
200
-
201
249
  return {
202
250
  fileList,
203
251
  getFileList,
204
252
  setFileList: setMergedFileList,
253
+ uploadFile,
205
254
  handlePasteFile: handleManualFile,
206
255
  handleDropFile: handleManualFile,
207
256
  uploadIconButton,
@@ -214,8 +263,10 @@ export default function useAttachments(
214
263
  fileList,
215
264
  getFileList,
216
265
  setFileList: setMergedFileList,
266
+ uploadFile,
217
267
  handlePasteFile: undefined,
218
268
  handleDropFile: undefined,
269
+ uploadFileListHeader,
219
270
  };
220
271
  }
221
272
  }