@agentscope-ai/chat 1.1.69 → 1.1.71-beta.1781610744021

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 (35) hide show
  1. package/components/AgentScopeRuntimeWebUI/core/Chat/Input/index.tsx +38 -5
  2. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.tsx +82 -0
  3. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/__tests__/inputQueue.test.ts +112 -0
  4. package/components/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.ts +122 -0
  5. package/components/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.tsx +111 -4
  6. package/components/AgentScopeRuntimeWebUI/core/Chat/index.tsx +21 -3
  7. package/components/AgentScopeRuntimeWebUI/core/Chat/styles.tsx +68 -1
  8. package/components/AgentScopeRuntimeWebUI/core/ChatAnywhere/index.tsx +1 -1
  9. package/components/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.tsx +14 -0
  10. package/components/AgentScopeRuntimeWebUI/starter/index.tsx +100 -14
  11. package/components/AgentScopeRuntimeWebUI/starterForMe/index.tsx +31 -0
  12. package/components/Attachments/index.tsx +30 -2
  13. package/components/ChatAnywhere/Input/index.tsx +5 -0
  14. package/components/ChatAnywhere/hooks/ChatAnywhereProvider.tsx +1 -0
  15. package/components/ChatAnywhere/hooks/types.ts +5 -0
  16. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.d.ts +8 -0
  17. package/lib/AgentScopeRuntimeWebUI/core/Chat/Input/index.js +36 -8
  18. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.d.ts +9 -0
  19. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/Panel.js +78 -0
  20. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.d.ts +37 -0
  21. package/lib/AgentScopeRuntimeWebUI/core/Chat/InputQueue/index.js +74 -0
  22. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.d.ts +7 -0
  23. package/lib/AgentScopeRuntimeWebUI/core/Chat/hooks/useChatController.js +204 -63
  24. package/lib/AgentScopeRuntimeWebUI/core/Chat/index.js +14 -2
  25. package/lib/AgentScopeRuntimeWebUI/core/Chat/styles.js +31 -1
  26. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.d.ts +11 -1
  27. package/lib/AgentScopeRuntimeWebUI/core/Context/ChatAnywhereI18nContext.js +12 -0
  28. package/lib/AgentScopeRuntimeWebUI/starter/index.js +144 -20
  29. package/lib/AgentScopeRuntimeWebUI/starterForMe/index.d.ts +1 -0
  30. package/lib/AgentScopeRuntimeWebUI/starterForMe/index.js +34 -0
  31. package/lib/Attachments/index.js +40 -2
  32. package/lib/ChatAnywhere/Input/index.js +8 -0
  33. package/lib/ChatAnywhere/hooks/ChatAnywhereProvider.d.ts +1 -0
  34. package/lib/ChatAnywhere/hooks/types.d.ts +5 -0
  35. package/package.json +2 -1
@@ -5,10 +5,19 @@ 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 InputQueuePanel from "../InputQueue/Panel";
9
+ import type { QueuedInputItem } from "../InputQueue";
8
10
 
9
11
  export interface InputProps {
10
12
  onCancel: () => void;
11
13
  onSubmit: (data: IAgentScopeRuntimeWebUIInputData) => void;
14
+ queue?: {
15
+ items: QueuedInputItem[];
16
+ onEnqueue: (data: IAgentScopeRuntimeWebUIInputData) => void;
17
+ onRemove: (id: string) => void;
18
+ onClear: () => void;
19
+ onRetry: (id: string) => void;
20
+ };
12
21
  }
13
22
 
14
23
  export default function Input(props: InputProps) {
@@ -44,18 +53,42 @@ export default function Input(props: InputProps) {
44
53
  if (!next) return;
45
54
 
46
55
  const fileList = (getFileList?.() || []).filter(i => i.response?.url);
47
- props.onSubmit({ query: getContent(), fileList });
56
+ const data = { query: getContent(), fileList };
57
+ if (inputContext.loading || props.queue?.items.length) {
58
+ props.queue?.onEnqueue(data);
59
+ } else {
60
+ props.onSubmit(data);
61
+ }
48
62
  setContent('');
49
- setFileList && setFileList([]);
50
- }, []);
63
+ setFileList?.([]);
64
+ }, [beforeSubmit, getContent, getFileList, inputContext.loading, props.onSubmit, props.queue, setContent, setFileList]);
65
+
66
+ const handleKeyDownCapture = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
67
+ if (event.key !== 'Enter' || event.shiftKey) return;
68
+ if (event.nativeEvent?.isComposing) return;
69
+ if (!inputContext.loading && !props.queue?.items.length) return;
70
+
71
+ const fileList = (getFileList?.() || []).filter(i => i.response?.url);
72
+ if (!getContent().trim() && fileList.length === 0) return;
73
+
74
+ event.preventDefault();
75
+ event.stopPropagation();
76
+ void handleSubmit();
77
+ }, [getContent, getFileList, handleSubmit, inputContext.loading, props.queue?.items.length]);
51
78
 
52
79
  const handleCancel = useCallback(() => {
53
80
  props.onCancel();
54
81
  }, []);
55
82
 
56
- return <div className={prefixCls}>
83
+ return <div className={prefixCls} onKeyDownCapture={handleKeyDownCapture}>
57
84
  <div className={`${prefixCls}-wrapper`}>
58
85
  {beforeUI}
86
+ {props.queue?.items.length ? <InputQueuePanel
87
+ items={props.queue.items}
88
+ onRemove={props.queue.onRemove}
89
+ onClear={props.queue.onClear}
90
+ onRetry={props.queue.onRetry}
91
+ /> : null}
59
92
  <ChatInput
60
93
  loading={inputContext.loading}
61
94
  disabled={inputContext.disabled}
@@ -80,4 +113,4 @@ export default function Input(props: InputProps) {
80
113
  disclaimer ? <Disclaimer desc={disclaimer} /> : <div className={`${prefixCls}-blank`}></div>
81
114
  }
82
115
  </div>;
83
- }
116
+ }
@@ -0,0 +1,82 @@
1
+ import { IconButton } from '@agentscope-ai/design';
2
+ import {
3
+ SparkClearLine,
4
+ SparkDeleteLine,
5
+ SparkRefreshLine,
6
+ } from '@agentscope-ai/icons';
7
+ import { Tooltip } from 'antd';
8
+ import { useProviderContext } from '@agentscope-ai/chat';
9
+ import { useTranslation } from '../../Context/ChatAnywhereI18nContext';
10
+ import type { QueuedInputItem } from './index';
11
+
12
+ interface InputQueuePanelProps {
13
+ items: QueuedInputItem[];
14
+ onRemove: (id: string) => void;
15
+ onClear: () => void;
16
+ onRetry: (id: string) => void;
17
+ }
18
+
19
+ export default function InputQueuePanel(props: InputQueuePanelProps) {
20
+ const { items, onRemove, onClear, onRetry } = props;
21
+ const prefixCls = useProviderContext().getPrefixCls('chat-anywhere-input-queue');
22
+ const { t } = useTranslation();
23
+ const tr = (key: Parameters<typeof t>[0]) => t?.(key) || key;
24
+
25
+ if (!items.length) return null;
26
+
27
+ return (
28
+ <div className={prefixCls}>
29
+ <div className={`${prefixCls}-header`}>
30
+ <span>
31
+ {tr('queue.title')} ({items.length})
32
+ </span>
33
+ <Tooltip title={tr('queue.clear')}>
34
+ <IconButton
35
+ size="small"
36
+ bordered={false}
37
+ icon={<SparkClearLine />}
38
+ onClick={onClear}
39
+ />
40
+ </Tooltip>
41
+ </div>
42
+ <div className={`${prefixCls}-list`}>
43
+ {items.map((item, index) => {
44
+ const failed = item.status === 'failed';
45
+ return (
46
+ <div className={`${prefixCls}-item`} key={item.id}>
47
+ <span className={`${prefixCls}-index`}>{index + 1}</span>
48
+ <div className={`${prefixCls}-content`}>
49
+ <div className={`${prefixCls}-text`}>
50
+ {item.data.query || tr('queue.attachmentOnly')}
51
+ </div>
52
+ {failed ? (
53
+ <div className={`${prefixCls}-error`}>
54
+ {item.errorMessage || tr('queue.failed')}
55
+ </div>
56
+ ) : null}
57
+ </div>
58
+ {failed ? (
59
+ <Tooltip title={tr('queue.retry')}>
60
+ <IconButton
61
+ size="small"
62
+ bordered={false}
63
+ icon={<SparkRefreshLine />}
64
+ onClick={() => onRetry(item.id)}
65
+ />
66
+ </Tooltip>
67
+ ) : null}
68
+ <Tooltip title={tr('common.delete')}>
69
+ <IconButton
70
+ size="small"
71
+ bordered={false}
72
+ icon={<SparkDeleteLine />}
73
+ onClick={() => onRemove(item.id)}
74
+ />
75
+ </Tooltip>
76
+ </div>
77
+ );
78
+ })}
79
+ </div>
80
+ </div>
81
+ );
82
+ }
@@ -0,0 +1,112 @@
1
+ import assert from 'node:assert/strict';
2
+ import {
3
+ canSubmitDirectly,
4
+ dequeueNextQueuedInput,
5
+ enqueueQueuedInput,
6
+ removeQueuedInput,
7
+ restoreFailedQueuedInput,
8
+ retryQueuedInput,
9
+ } from '../index';
10
+
11
+ const input = (query: string) => ({ query });
12
+
13
+ function test(name: string, fn: () => void) {
14
+ try {
15
+ fn();
16
+ console.log(`ok - ${name}`);
17
+ } catch (error) {
18
+ console.error(`not ok - ${name}`);
19
+ throw error;
20
+ }
21
+ }
22
+
23
+ test('enqueue appends inputs in FIFO order and dequeue consumes the head', () => {
24
+ let queue = enqueueQueuedInput([], input('first'), {
25
+ id: 'q1',
26
+ now: 1,
27
+ }).queue;
28
+ queue = enqueueQueuedInput(queue, input('second'), {
29
+ id: 'q2',
30
+ now: 2,
31
+ }).queue;
32
+
33
+ const first = dequeueNextQueuedInput(queue);
34
+ assert.equal(first.item?.data.query, 'first');
35
+ assert.deepEqual(first.queue.map(item => item.data.query), ['second']);
36
+
37
+ const second = dequeueNextQueuedInput(first.queue);
38
+ assert.equal(second.item?.data.query, 'second');
39
+ assert.equal(second.queue.length, 0);
40
+ });
41
+
42
+ test('direct submit is allowed only when idle, queue is empty and no drain is active', () => {
43
+ assert.equal(
44
+ canSubmitDirectly({ loading: false, queueLength: 0, draining: false }),
45
+ true,
46
+ );
47
+ assert.equal(
48
+ canSubmitDirectly({ loading: true, queueLength: 0, draining: false }),
49
+ false,
50
+ );
51
+ assert.equal(
52
+ canSubmitDirectly({ loading: false, queueLength: 1, draining: false }),
53
+ false,
54
+ );
55
+ assert.equal(
56
+ canSubmitDirectly({ loading: false, queueLength: 0, draining: true }),
57
+ false,
58
+ );
59
+ });
60
+
61
+ test('enqueue rejects new input when the queue reaches max size', () => {
62
+ const queue = enqueueQueuedInput([], input('first'), {
63
+ id: 'q1',
64
+ maxSize: 1,
65
+ }).queue;
66
+ const result = enqueueQueuedInput(queue, input('second'), {
67
+ id: 'q2',
68
+ maxSize: 1,
69
+ });
70
+
71
+ assert.equal(result.reason, 'full');
72
+ assert.deepEqual(result.queue.map(item => item.id), ['q1']);
73
+ });
74
+
75
+ test('failed send is restored at the queue head and blocks automatic dequeue', () => {
76
+ const original = enqueueQueuedInput([], input('send me'), {
77
+ id: 'q1',
78
+ }).queue[0];
79
+ const queue = restoreFailedQueuedInput([], original, new Error('network'));
80
+
81
+ assert.equal(queue[0].status, 'failed');
82
+ assert.equal(queue[0].retryCount, 1);
83
+ assert.equal(queue[0].errorMessage, 'network');
84
+
85
+ const next = dequeueNextQueuedInput(queue);
86
+ assert.equal(next.item, undefined);
87
+ assert.equal(next.queue, queue);
88
+ });
89
+
90
+ test('retry marks a failed item pending so it can be sent again', () => {
91
+ const original = enqueueQueuedInput([], input('retry me'), {
92
+ id: 'q1',
93
+ }).queue[0];
94
+ const failed = restoreFailedQueuedInput([], original, 'boom');
95
+ const retried = retryQueuedInput(failed, 'q1');
96
+ const next = dequeueNextQueuedInput(retried);
97
+
98
+ assert.equal(retried[0].status, 'pending');
99
+ assert.equal(retried[0].errorMessage, undefined);
100
+ assert.equal(next.item?.id, 'q1');
101
+ });
102
+
103
+ test('remove deletes only the selected queued input', () => {
104
+ let queue = enqueueQueuedInput([], input('first'), { id: 'q1' }).queue;
105
+ queue = enqueueQueuedInput(queue, input('second'), { id: 'q2' }).queue;
106
+
107
+ assert.deepEqual(removeQueuedInput(queue, 'q1').map(item => item.id), ['q2']);
108
+ assert.deepEqual(removeQueuedInput(queue, 'missing').map(item => item.id), [
109
+ 'q1',
110
+ 'q2',
111
+ ]);
112
+ });
@@ -0,0 +1,122 @@
1
+ import type { IAgentScopeRuntimeWebUIInputData } from '../../types';
2
+
3
+ export type QueuedInputStatus = 'pending' | 'failed';
4
+
5
+ export interface QueuedInputItem {
6
+ id: string;
7
+ data: IAgentScopeRuntimeWebUIInputData;
8
+ status: QueuedInputStatus;
9
+ retryCount: number;
10
+ errorMessage?: string;
11
+ createdAt: number;
12
+ }
13
+
14
+ export interface EnqueueQueuedInputResult {
15
+ queue: QueuedInputItem[];
16
+ item?: QueuedInputItem;
17
+ reason?: 'full';
18
+ }
19
+
20
+ export const MAX_INPUT_QUEUE_SIZE = 50;
21
+
22
+ let queueId = 0;
23
+
24
+ export function createQueuedInputItem(
25
+ data: IAgentScopeRuntimeWebUIInputData,
26
+ options?: {
27
+ id?: string;
28
+ now?: number;
29
+ },
30
+ ): QueuedInputItem {
31
+ return {
32
+ id:
33
+ options?.id ||
34
+ `input-queue-${Date.now().toString(36)}-${(++queueId).toString(36)}`,
35
+ data,
36
+ status: 'pending',
37
+ retryCount: 0,
38
+ createdAt: options?.now ?? Date.now(),
39
+ };
40
+ }
41
+
42
+ export function enqueueQueuedInput(
43
+ queue: QueuedInputItem[],
44
+ data: IAgentScopeRuntimeWebUIInputData,
45
+ options?: {
46
+ maxSize?: number;
47
+ id?: string;
48
+ now?: number;
49
+ },
50
+ ): EnqueueQueuedInputResult {
51
+ const maxSize = options?.maxSize ?? MAX_INPUT_QUEUE_SIZE;
52
+ if (queue.length >= maxSize) {
53
+ return { queue, reason: 'full' };
54
+ }
55
+
56
+ const item = createQueuedInputItem(data, options);
57
+ return {
58
+ queue: [...queue, item],
59
+ item,
60
+ };
61
+ }
62
+
63
+ export function dequeueNextQueuedInput(queue: QueuedInputItem[]): {
64
+ item?: QueuedInputItem;
65
+ queue: QueuedInputItem[];
66
+ } {
67
+ const next = queue[0];
68
+ if (!next || next.status !== 'pending') {
69
+ return { queue };
70
+ }
71
+
72
+ return {
73
+ item: next,
74
+ queue: queue.slice(1),
75
+ };
76
+ }
77
+
78
+ export function removeQueuedInput(
79
+ queue: QueuedInputItem[],
80
+ id: string,
81
+ ): QueuedInputItem[] {
82
+ return queue.filter(item => item.id !== id);
83
+ }
84
+
85
+ export function retryQueuedInput(
86
+ queue: QueuedInputItem[],
87
+ id: string,
88
+ ): QueuedInputItem[] {
89
+ return queue.map(item =>
90
+ item.id === id
91
+ ? {
92
+ ...item,
93
+ status: 'pending',
94
+ errorMessage: undefined,
95
+ }
96
+ : item,
97
+ );
98
+ }
99
+
100
+ export function restoreFailedQueuedInput(
101
+ queue: QueuedInputItem[],
102
+ item: QueuedInputItem,
103
+ error?: unknown,
104
+ ): QueuedInputItem[] {
105
+ return [
106
+ {
107
+ ...item,
108
+ status: 'failed',
109
+ retryCount: item.retryCount + 1,
110
+ errorMessage: error instanceof Error ? error.message : String(error || ''),
111
+ },
112
+ ...queue,
113
+ ];
114
+ }
115
+
116
+ export function canSubmitDirectly(options: {
117
+ loading: boolean | string;
118
+ queueLength: number;
119
+ draining: boolean;
120
+ }) {
121
+ return !options.loading && options.queueLength === 0 && !options.draining;
122
+ }
@@ -1,11 +1,21 @@
1
1
  import { sleep } from "@agentscope-ai/chat";
2
- import { useCallback, useEffect, useRef } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { useContextSelector } from "use-context-selector";
4
4
  import { ChatAnywhereInputContext } from "../../Context/ChatAnywhereInputContext";
5
5
  import { ChatAnywhereSessionsContext } from "../../Context/ChatAnywhereSessionsContext";
6
6
  import useChatAnywhereEventEmitter from "../../Context/useChatAnywhereEventEmitter";
7
7
  import { IAgentScopeRuntimeWebUIMessage } from "@agentscope-ai/chat";
8
8
  import { InputProps } from "../Input";
9
+ import {
10
+ canSubmitDirectly,
11
+ dequeueNextQueuedInput,
12
+ enqueueQueuedInput,
13
+ MAX_INPUT_QUEUE_SIZE,
14
+ removeQueuedInput,
15
+ restoreFailedQueuedInput,
16
+ retryQueuedInput,
17
+ type QueuedInputItem,
18
+ } from "../InputQueue";
9
19
  import useChatMessageHandler from "./useChatMessageHandler";
10
20
  import useChatRequest from "./useChatRequest";
11
21
  import useChatSessionHandler from "./useChatSessionHandler";
@@ -18,6 +28,7 @@ import ReactDOM from "react-dom";
18
28
  */
19
29
  export default function useChatController() {
20
30
  const setLoading = useContextSelector(ChatAnywhereInputContext, v => v.setLoading);
31
+ const getLoading = useContextSelector(ChatAnywhereInputContext, v => v.getLoading);
21
32
  const currentSessionId = useContextSelector(ChatAnywhereSessionsContext, v => v.currentSessionId);
22
33
  const apiOptions = useChatAnywhereOptions(v => v.api);
23
34
  const apiOptionsRef = useRef(apiOptions);
@@ -43,6 +54,15 @@ export default function useChatController() {
43
54
  */
44
55
  activeSessionId?: string;
45
56
  }>({ activeRequestId: 0 });
57
+ const [inputQueue, setInputQueue] = useState<QueuedInputItem[]>([]);
58
+ const inputQueueRef = useRef<QueuedInputItem[]>([]);
59
+ const drainTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
60
+ const drainingRef = useRef(false);
61
+ const drainQueueRef = useRef<(() => Promise<void>) | null>(null);
62
+
63
+ useEffect(() => {
64
+ inputQueueRef.current = inputQueue;
65
+ }, [inputQueue]);
46
66
 
47
67
  // Message handler
48
68
  const messageHandler = useChatMessageHandler({ currentQARef });
@@ -53,6 +73,17 @@ export default function useChatController() {
53
73
  /**
54
74
  * Finalize the current response and reset UI loading state.
55
75
  */
76
+ const scheduleDrainQueue = useCallback(() => {
77
+ if (drainTimerRef.current) {
78
+ clearTimeout(drainTimerRef.current);
79
+ }
80
+
81
+ drainTimerRef.current = setTimeout(() => {
82
+ drainTimerRef.current = null;
83
+ void drainQueueRef.current?.();
84
+ }, 0);
85
+ }, []);
86
+
56
87
  const finishResponse = useCallback((status: 'finished' | 'interrupted' = 'finished') => {
57
88
  if (!currentQARef.current.response) return;
58
89
 
@@ -63,7 +94,8 @@ export default function useChatController() {
63
94
  });
64
95
 
65
96
  sessionHandler.syncSessionMessages(messageHandler.getMessages());
66
- }, [setLoading, messageHandler, sessionHandler]);
97
+ scheduleDrainQueue();
98
+ }, [setLoading, messageHandler, sessionHandler, scheduleDrainQueue]);
67
99
 
68
100
  // API request handling
69
101
  const { request, reconnect } = useChatRequest({
@@ -76,7 +108,7 @@ export default function useChatController() {
76
108
  /**
77
109
  * Handle user message submission.
78
110
  */
79
- const handleSubmit = useCallback<InputProps['onSubmit']>(async (data) => {
111
+ const submitNow = useCallback<InputProps['onSubmit']>(async (data) => {
80
112
  // 0. Abort any previous in-flight SSE. We do NOT call the cancel API here
81
113
  // — the user is sending a new message, not explicitly cancelling.
82
114
  // Cancel is only invoked from handleCancel.
@@ -120,6 +152,61 @@ export default function useChatController() {
120
152
  // mockRequest(mockdata);
121
153
  }, [messageHandler, sessionHandler, request, setLoading]);
122
154
 
155
+ const enqueueInput = useCallback((data: Parameters<InputProps['onSubmit']>[0]) => {
156
+ const result = enqueueQueuedInput(inputQueueRef.current, data, {
157
+ maxSize: MAX_INPUT_QUEUE_SIZE,
158
+ });
159
+ inputQueueRef.current = result.queue;
160
+ setInputQueue(result.queue);
161
+ }, []);
162
+
163
+ const drainQueue = useCallback(async () => {
164
+ if (drainingRef.current || getLoading()) return;
165
+
166
+ const result = dequeueNextQueuedInput(inputQueueRef.current);
167
+ const nextItem = result.item;
168
+
169
+ if (!nextItem) return;
170
+
171
+ inputQueueRef.current = result.queue;
172
+ setInputQueue(result.queue);
173
+
174
+ drainingRef.current = true;
175
+ try {
176
+ await submitNow(nextItem.data);
177
+ } catch (error) {
178
+ setLoading(false);
179
+ const restored = restoreFailedQueuedInput(inputQueueRef.current, nextItem, error);
180
+ inputQueueRef.current = restored;
181
+ setInputQueue(restored);
182
+ } finally {
183
+ drainingRef.current = false;
184
+ }
185
+ }, [getLoading, setLoading, submitNow]);
186
+
187
+ useEffect(() => {
188
+ drainQueueRef.current = drainQueue;
189
+ }, [drainQueue]);
190
+
191
+ const handleSubmit = useCallback<InputProps['onSubmit']>(async (data) => {
192
+ if (!canSubmitDirectly({
193
+ loading: getLoading(),
194
+ queueLength: inputQueueRef.current.length,
195
+ draining: drainingRef.current,
196
+ })) {
197
+ enqueueInput(data);
198
+ return;
199
+ }
200
+
201
+ await submitNow(data);
202
+ }, [enqueueInput, getLoading, submitNow]);
203
+
204
+ useEffect(() => {
205
+ if (!getLoading() && inputQueue.length > 0) {
206
+ scheduleDrainQueue();
207
+ }
208
+ }, [getLoading, inputQueue.length, scheduleDrainQueue]);
209
+
123
210
 
124
211
  const handleApproval = useCallback(async ({ input }) => {
125
212
  currentQARef.current.abortController?.abort();
@@ -259,6 +346,10 @@ export default function useChatController() {
259
346
  };
260
347
 
261
348
  return () => {
349
+ if (drainTimerRef.current) {
350
+ clearTimeout(drainTimerRef.current);
351
+ drainTimerRef.current = null;
352
+ }
262
353
  currentQARef.current.abortController?.abort();
263
354
  currentQARef.current.activeRequestId += 1;
264
355
  };
@@ -298,6 +389,22 @@ export default function useChatController() {
298
389
  return {
299
390
  handleSubmit,
300
391
  handleCancel,
392
+ inputQueue,
393
+ enqueueQueuedInput: enqueueInput,
394
+ removeQueuedInput: (id: string) => {
395
+ const next = removeQueuedInput(inputQueueRef.current, id);
396
+ inputQueueRef.current = next;
397
+ setInputQueue(next);
398
+ },
399
+ clearQueuedInputs: () => {
400
+ inputQueueRef.current = [];
401
+ setInputQueue([]);
402
+ },
403
+ retryQueuedInput: (id: string) => {
404
+ const next = retryQueuedInput(inputQueueRef.current, id);
405
+ inputQueueRef.current = next;
406
+ setInputQueue(next);
407
+ scheduleDrainQueue();
408
+ },
301
409
  };
302
410
  }
303
-
@@ -7,14 +7,32 @@ import { useChatAnywhereSessionLoader } from "../Context/ChatAnywhereSessionsCon
7
7
 
8
8
  export default function Chat() {
9
9
  const prefixCls = useProviderContext().getPrefixCls('chat-anywhere-chat');
10
- const { handleSubmit, handleCancel } = useChatController();
10
+ const {
11
+ handleSubmit,
12
+ handleCancel,
13
+ inputQueue,
14
+ enqueueQueuedInput,
15
+ removeQueuedInput,
16
+ clearQueuedInputs,
17
+ retryQueuedInput,
18
+ } = useChatController();
11
19
  useChatAnywhereSessionLoader();
12
20
 
13
21
  return <>
14
22
  <Style />
15
23
  <div className={prefixCls}>
16
24
  <MessageList onSubmit={handleSubmit} />
17
- <Input onCancel={handleCancel} onSubmit={handleSubmit} />
25
+ <Input
26
+ onCancel={handleCancel}
27
+ onSubmit={handleSubmit}
28
+ queue={{
29
+ items: inputQueue,
30
+ onEnqueue: enqueueQueuedInput,
31
+ onRemove: removeQueuedInput,
32
+ onClear: clearQueuedInputs,
33
+ onRetry: retryQueuedInput,
34
+ }}
35
+ />
18
36
  </div>
19
37
  </>;
20
- }
38
+ }
@@ -54,6 +54,73 @@ export default createGlobalStyle`
54
54
  min-width: 300px;
55
55
  margin: 0 auto;
56
56
  }
57
+
58
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue {
59
+ display: flex;
60
+ flex-direction: column;
61
+ gap: 6px;
62
+ margin-bottom: 8px;
63
+ padding: 8px;
64
+ border: 1px solid ${(p) => p.theme.colorBorderSecondary};
65
+ border-radius: 8px;
66
+ background: ${(p) => p.theme.colorFillTertiary};
67
+ }
68
+
69
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-header {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: space-between;
73
+ color: ${(p) => p.theme.colorTextSecondary};
74
+ font-size: 12px;
75
+ line-height: 20px;
76
+ }
77
+
78
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-list {
79
+ display: flex;
80
+ max-height: 180px;
81
+ flex-direction: column;
82
+ gap: 4px;
83
+ overflow-y: auto;
84
+ }
85
+
86
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-item {
87
+ display: grid;
88
+ grid-template-columns: 20px minmax(0, 1fr) auto auto;
89
+ align-items: center;
90
+ gap: 6px;
91
+ min-height: 32px;
92
+ padding: 4px 6px;
93
+ border-radius: 6px;
94
+ background: ${(p) => p.theme.colorBgContainer};
95
+ }
96
+
97
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-index {
98
+ color: ${(p) => p.theme.colorTextQuaternary};
99
+ font-size: 12px;
100
+ text-align: center;
101
+ }
102
+
103
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-content {
104
+ min-width: 0;
105
+ }
106
+
107
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-text {
108
+ overflow: hidden;
109
+ color: ${(p) => p.theme.colorText};
110
+ font-size: 12px;
111
+ line-height: 18px;
112
+ text-overflow: ellipsis;
113
+ white-space: nowrap;
114
+ }
115
+
116
+ .${(p) => p.theme.prefixCls}-chat-anywhere-input-queue-error {
117
+ overflow: hidden;
118
+ color: ${(p) => p.theme.colorError};
119
+ font-size: 11px;
120
+ line-height: 16px;
121
+ text-overflow: ellipsis;
122
+ white-space: nowrap;
123
+ }
57
124
  .${(p) => p.theme.prefixCls}-chat-anywhere-input-blank {
58
125
  height: 16px;
59
- `;
126
+ `;
@@ -1,6 +1,6 @@
1
1
  import Layout from '../Layout';
2
2
  import type { IAgentScopeRuntimeWebUIOptions } from '@agentscope-ai/chat';
3
- import { forwardRef, useMemo, useState } from 'react';
3
+ import { forwardRef, useMemo } from 'react';
4
4
  import AgentScopeRuntimeRequestCard from '../AgentScopeRuntime/Request/Card';
5
5
  import AgentScopeRuntimeResponseCard from '../AgentScopeRuntime/Response/Card';
6
6
  import ComposedProvider from './ComposedProvider';