diff --git a/components/useXChat/__tests__/__snapshots__/demo.test.ts.snap b/components/useXChat/__tests__/__snapshots__/demo.test.ts.snap
index bea7dad7..2699e1b6 100644
--- a/components/useXChat/__tests__/__snapshots__/demo.test.ts.snap
+++ b/components/useXChat/__tests__/__snapshots__/demo.test.ts.snap
@@ -116,6 +116,64 @@ exports[`renders components/useXChat/demo/stream.tsx correctly 1`] = `
`;
+exports[`renders components/useXChat/demo/stream-cancel.tsx correctly 1`] = `
+
+`;
+
exports[`renders components/useXChat/demo/suggestions.tsx correctly 1`] = `
= {
+ ai: {
+ placement: 'start',
+ avatar: { icon: , style: { background: '#fde3cf' } },
+ },
+ local: {
+ placement: 'end',
+ avatar: { icon: , style: { background: '#87d068' } },
+ },
+};
+
+const contentChunks = [
+ 'He',
+ 'llo',
+ ', w',
+ 'or',
+ 'ld!',
+ ' Ant',
+ ' Design',
+ ' X',
+ ' is',
+ ' the',
+ ' best',
+ '!',
+];
+
+function mockReadableStream() {
+ const sseChunks: string[] = [];
+
+ for (let i = 0; i < contentChunks.length; i++) {
+ const sseEventPart = `event: message\ndata: {"id":"${i}","content":"${contentChunks[i]}"}\n\n`;
+ sseChunks.push(sseEventPart);
+ }
+
+ return new ReadableStream({
+ async start(controller) {
+ for (const chunk of sseChunks) {
+ await new Promise((resolve) => setTimeout(resolve, 300));
+ controller.enqueue(new TextEncoder().encode(chunk));
+ }
+ controller.close();
+ },
+ });
+}
+
+const App = () => {
+ const [content, setContent] = React.useState('');
+
+ const abortRef = useRef(() => {});
+
+ useEffect(() => {
+ return () => {
+ abortRef.current();
+ };
+ }, []);
+
+ // Agent for request
+ const [agent] = useXAgent({
+ request: async (_, { onSuccess, onUpdate }) => {
+ const stream = XStream({
+ readableStream: mockReadableStream(),
+ });
+
+ const reader = stream.getReader();
+ abortRef.current = () => {
+ reader?.cancel();
+ };
+
+ let current = '';
+ while (reader) {
+ const { value, done } = await reader.read();
+ if (done) {
+ onSuccess(current);
+ break;
+ }
+ if (!value) continue;
+ const data = JSON.parse(value.data);
+ current += data.content || '';
+ onUpdate(current);
+ }
+ },
+ });
+
+ // Chat messages
+ const { onRequest, messages } = useXChat({
+ agent,
+ });
+
+ return (
+
+ ({
+ key: id,
+ role: status === 'local' ? 'local' : 'ai',
+ content: message,
+ }))}
+ />
+ {
+ onRequest(nextContent);
+ setContent('');
+ }}
+ onCancel={() => abortRef.current()}
+ />
+
+ );
+};
+
+export default App;
diff --git a/components/useXChat/index.en-US.md b/components/useXChat/index.en-US.md
index 3b4cba62..416a7544 100644
--- a/components/useXChat/index.en-US.md
+++ b/components/useXChat/index.en-US.md
@@ -20,6 +20,7 @@ Use Agent to manage conversation data and produce data for page rendering.
Basic
Streaming
+Interrupt the output
Multiple Suggestion
## API
diff --git a/components/useXChat/index.zh-CN.md b/components/useXChat/index.zh-CN.md
index 5f5f10a3..4477cadc 100644
--- a/components/useXChat/index.zh-CN.md
+++ b/components/useXChat/index.zh-CN.md
@@ -21,6 +21,7 @@ demo:
基本
流式输出
+打断输出
多项建议
## API
diff --git a/components/x-stream/__tests__/index.test.tsx b/components/x-stream/__tests__/index.test.tsx
index feee60f4..8d394dd8 100644
--- a/components/x-stream/__tests__/index.test.tsx
+++ b/components/x-stream/__tests__/index.test.tsx
@@ -97,4 +97,17 @@ describe('XStream', () => {
})(),
).rejects.toThrow('The key-value separator ":" is not found in the sse line chunk!');
});
+
+ it('should return an instance of ReadableStream', () => {
+ expect(
+ XStream({
+ readableStream: new ReadableStream({
+ async start(controller) {
+ controller.enqueue(new TextEncoder().encode('event: message\n\ndata: value\n\n'));
+ controller.close();
+ },
+ }),
+ }),
+ ).toBeInstanceOf(ReadableStream);
+ });
});
diff --git a/components/x-stream/index.ts b/components/x-stream/index.ts
index 6868ae61..6ae18a98 100644
--- a/components/x-stream/index.ts
+++ b/components/x-stream/index.ts
@@ -137,11 +137,13 @@ export interface XStreamOptions