@agentscope-ai/chat 1.1.68 → 1.1.70

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 (28) hide show
  1. package/bin/starter_webui/README.md +75 -0
  2. package/bin/starter_webui/eslint.config.js +28 -0
  3. package/bin/starter_webui/index.html +12 -0
  4. package/bin/starter_webui/package.json +34 -0
  5. package/bin/starter_webui/src/App.tsx +20 -0
  6. package/bin/starter_webui/src/components/Chat/OptionsPanel/FormItem.tsx +37 -0
  7. package/bin/starter_webui/src/components/Chat/OptionsPanel/OptionsEditor.tsx +160 -0
  8. package/bin/starter_webui/src/components/Chat/OptionsPanel/defaultConfig.ts +41 -0
  9. package/bin/starter_webui/src/components/Chat/OptionsPanel/index.tsx +27 -0
  10. package/bin/starter_webui/src/components/Chat/index.tsx +45 -0
  11. package/bin/starter_webui/src/components/Chat/sessionApi/index.ts +53 -0
  12. package/bin/starter_webui/src/main.tsx +9 -0
  13. package/bin/starter_webui/src/vite-env.d.ts +4 -0
  14. package/bin/starter_webui/tsconfig.app.json +24 -0
  15. package/bin/starter_webui/tsconfig.json +7 -0
  16. package/bin/starter_webui/tsconfig.node.json +22 -0
  17. package/bin/starter_webui/vite.config.ts +11 -0
  18. package/components/Attachments/index.tsx +30 -2
  19. package/components/ChatAnywhere/Input/index.tsx +5 -0
  20. package/components/ChatAnywhere/hooks/ChatAnywhereProvider.tsx +1 -0
  21. package/components/ChatAnywhere/hooks/types.ts +5 -0
  22. package/components/OperateCard/preset/Rag.tsx +22 -5
  23. package/lib/Attachments/index.js +40 -2
  24. package/lib/ChatAnywhere/Input/index.js +8 -0
  25. package/lib/ChatAnywhere/hooks/ChatAnywhereProvider.d.ts +1 -0
  26. package/lib/ChatAnywhere/hooks/types.d.ts +5 -0
  27. package/lib/OperateCard/preset/Rag.js +20 -4
  28. package/package.json +1 -1
@@ -0,0 +1,75 @@
1
+ # agentscope-runtime-starter-webui
2
+
3
+ ## node version
4
+
5
+ > =22
6
+
7
+ ## install
8
+
9
+ ```
10
+ $ npm run install
11
+ ```
12
+
13
+ ## dev
14
+
15
+ ```
16
+ $ npm run dev
17
+ ```
18
+
19
+ ## build
20
+
21
+ ```
22
+ $ npm run build
23
+ ```
24
+
25
+ ## Core Code
26
+ ```tsx
27
+ import { AgentScopeRuntimeWebUI } from '@agentscope-ai/chat';
28
+
29
+ const options = {
30
+ theme: {
31
+ colorPrimary: '#615CED',
32
+ darkMode: true,
33
+ prefix: 'agentscope-runtime-webui',
34
+ leftHeader: {
35
+ logo: 'https://img.alicdn.com/imgextra/i2/O1CN01lmoGYn1kjoXATy4PX_!!6000000004720-2-tps-200-200.png',
36
+ title: 'Runtime WebUI',
37
+ },
38
+ },
39
+ sender: {
40
+ maxLength: 10000,
41
+ disclaimer:
42
+ 'AI can also make mistakes, so please check carefully and use it with caution',
43
+ },
44
+
45
+ welcome: {
46
+ greeting: 'Hello, how can I help you today?',
47
+ description:
48
+ 'I am a helpful assistant that can help you with your questions.',
49
+ avatar:
50
+ 'https://img.alicdn.com/imgextra/i2/O1CN01lmoGYn1kjoXATy4PX_!!6000000004720-2-tps-200-200.png',
51
+ prompts: [
52
+ {
53
+ value: 'Hello',
54
+ },
55
+ {
56
+ value: 'How are you?',
57
+ },
58
+ {
59
+ value: 'What can you do?',
60
+ },
61
+ ],
62
+ },
63
+ api: {
64
+ baseURL: 'YOUR_API_URL',
65
+ token: 'YOUR_API_TOKEN', // is not required
66
+ },
67
+ };
68
+
69
+
70
+ <AgentScopeRuntimeWebUI
71
+ options={options}
72
+ />
73
+
74
+
75
+ ```
@@ -0,0 +1,28 @@
1
+ import js from '@eslint/js'
2
+ import globals from 'globals'
3
+ import reactHooks from 'eslint-plugin-react-hooks'
4
+ import reactRefresh from 'eslint-plugin-react-refresh'
5
+ import tseslint from 'typescript-eslint'
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': [
23
+ 'warn',
24
+ { allowConstantExport: true },
25
+ ],
26
+ },
27
+ },
28
+ )
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AgentScope Runtime Starter WebUI</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "agentscope-runtime-starter-webui",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --host",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview"
11
+ },
12
+ "dependencies": {
13
+ "@agentscope-ai/icons": "^1.0.46",
14
+ "@agentscope-ai/chat": "^1.1.44",
15
+ "@agentscope-ai/design": "^1.0.19",
16
+ "antd": "^5.29.1",
17
+ "antd-style": "^3.7.1",
18
+ "react": "^18",
19
+ "react-dom": "^18"
20
+ },
21
+ "devDependencies": {
22
+ "@eslint/js": "^9.25.0",
23
+ "@types/react": "^18",
24
+ "@types/react-dom": "^18",
25
+ "@vitejs/plugin-react": "^4.4.1",
26
+ "eslint": "^9.25.0",
27
+ "eslint-plugin-react-hooks": "^5.2.0",
28
+ "eslint-plugin-react-refresh": "^0.4.19",
29
+ "globals": "^16.0.0",
30
+ "typescript": "~5.8.3",
31
+ "typescript-eslint": "^8.30.1",
32
+ "vite": "^6.3.5"
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ import Chat from './components/Chat';
2
+
3
+ import { createGlobalStyle } from 'antd-style';
4
+
5
+
6
+ const GlobalStyle = createGlobalStyle`
7
+ * {
8
+ margin: 0;
9
+ box-sizing: border-box;
10
+ }
11
+ `;
12
+
13
+ function App() {
14
+ return <>
15
+ <GlobalStyle />
16
+ <Chat />
17
+ </>
18
+ }
19
+
20
+ export default App
@@ -0,0 +1,37 @@
1
+ import { Form } from 'antd';
2
+ import { createStyles } from 'antd-style';
3
+
4
+
5
+ interface FormItemProps {
6
+ name: string | string[];
7
+ label: string;
8
+ isList?: boolean;
9
+ children: any;
10
+ normalize?: (value: any) => any;
11
+ }
12
+
13
+
14
+ const useStyles = createStyles(({ token }) => ({
15
+ label: {
16
+ marginBottom: 6,
17
+ fontSize: 12,
18
+ color: token.colorTextSecondary,
19
+ },
20
+
21
+ }));
22
+
23
+ export default function FormItem(props: FormItemProps) {
24
+ const { styles } = useStyles();
25
+
26
+
27
+ const node = props.isList ?
28
+ <Form.List name={props.name}>{props.children}</Form.List> :
29
+ <Form.Item name={props.name} normalize={props.normalize}>{props.children}</Form.Item>;
30
+
31
+
32
+ return <div>
33
+ {props.label && <div className={styles.label}>{props.label}</div>}
34
+ {node}
35
+ </div>
36
+
37
+ }
@@ -0,0 +1,160 @@
1
+ import React from 'react';
2
+ import { Form, Input, ColorPicker, Flex, Divider, InputNumber } from 'antd';
3
+ import { createStyles } from 'antd-style';
4
+ import { Button, IconButton, Switch } from '@agentscope-ai/design'
5
+ import { SparkDeleteLine, SparkPlusLine } from '@agentscope-ai/icons';
6
+ import FormItem from './FormItem';
7
+ import defaultConfig from './defaultConfig';
8
+
9
+ const useStyles = createStyles(({ token }) => ({
10
+ container: {
11
+ height: '100%',
12
+ display: 'flex',
13
+ flexDirection: 'column',
14
+ },
15
+
16
+ form: {
17
+ height: 0,
18
+ flex: 1,
19
+ padding: '8px 16px 16px 16px',
20
+ overflow: 'auto',
21
+ },
22
+ actions: {
23
+ padding: 16,
24
+ display: 'flex',
25
+ borderTop: `1px solid ${token.colorBorderSecondary}`,
26
+ justifyContent: 'flex-end',
27
+ gap: 16,
28
+ }
29
+
30
+ }));
31
+
32
+ interface OptionsEditorProps {
33
+ value?: any;
34
+ onChange?: any;
35
+ }
36
+
37
+ const OptionsEditor: React.FC<OptionsEditorProps> = ({
38
+ value,
39
+ onChange,
40
+ }) => {
41
+ const { styles } = useStyles();
42
+ const [form] = Form.useForm();
43
+
44
+
45
+ const handleSave = () => {
46
+ form.validateFields().then((values) => {
47
+ onChange(values);
48
+ });
49
+ };
50
+
51
+ const handleReset = () => {
52
+ form.setFieldsValue(defaultConfig);
53
+ };
54
+
55
+ return (
56
+ <div className={styles.container}>
57
+ <Form
58
+ className={styles.form}
59
+ form={form}
60
+ layout="vertical"
61
+ initialValues={value}
62
+ >
63
+
64
+
65
+ <Divider orientation="left">Theme</Divider>
66
+
67
+ <FormItem name={['theme', 'colorPrimary']} label="colorPrimary" normalize={value => value.toHexString()}>
68
+ <ColorPicker />
69
+ </FormItem>
70
+
71
+ <FormItem name={['theme', 'colorBgBase']} label="colorBgBase" normalize={value => value.toHexString()}>
72
+ <ColorPicker />
73
+ </FormItem>
74
+
75
+ <FormItem name={['theme', 'colorTextBase']} label="colorTextBase" normalize={value => value.toHexString()}>
76
+ <ColorPicker />
77
+ </FormItem>
78
+
79
+ <FormItem name={['theme', 'darkMode']} label="darkMode" >
80
+ <Switch />
81
+ </FormItem>
82
+
83
+ <FormItem name={['theme', 'leftHeader', 'logo']} label="leftHeader.logo" >
84
+ <Input />
85
+ </FormItem>
86
+
87
+ <FormItem name={['theme', 'leftHeader', 'title']} label="leftHeader.title" >
88
+ <Input />
89
+ </FormItem>
90
+
91
+ <Divider orientation="left">Sender</Divider>
92
+
93
+
94
+ <FormItem name={['sender', 'disclaimer']} label="disclaimer" >
95
+ <Input />
96
+ </FormItem>
97
+
98
+
99
+
100
+
101
+ <FormItem name={['sender', 'maxLength']} label="maxLength" >
102
+ <InputNumber min={1000} />
103
+ </FormItem>
104
+
105
+ <Divider orientation="left">Welcome</Divider>
106
+
107
+
108
+ <FormItem name={['welcome', 'greeting']} label="greeting" >
109
+ <Input />
110
+ </FormItem>
111
+
112
+ <FormItem name={['welcome', 'description']} label="description" >
113
+ <Input />
114
+ </FormItem>
115
+
116
+ <FormItem name={['welcome', 'avatar']} label="avatar" >
117
+ <Input />
118
+ </FormItem>
119
+
120
+
121
+ <FormItem name={['welcome', 'prompts']} isList label="prompts" >
122
+ {(fields: { key: string, name: string }[], { add, remove }: { add: (item: any) => void, remove: (name: string) => void }) => {
123
+ return <div>
124
+ {fields.map(field => {
125
+ return <Flex key={field.key} gap={6}>
126
+ <Form.Item style={{ flex: 1 }} key={field.key} name={[field.name, 'value']}>
127
+ <Input />
128
+ </Form.Item>
129
+ <IconButton icon={<SparkPlusLine />} onClick={() => add({})}></IconButton>
130
+ <IconButton icon={<SparkDeleteLine />} onClick={() => remove(field.name)}></IconButton>
131
+ </Flex>
132
+ })}
133
+ </div>
134
+ }}
135
+ </FormItem>
136
+
137
+
138
+ <Divider orientation="left">API</Divider>
139
+
140
+ <FormItem name={['api', 'baseURL']} label="baseURL" >
141
+ <Input />
142
+ </FormItem>
143
+
144
+ <FormItem name={['api', 'token']} label="token" >
145
+ <Input />
146
+ </FormItem>
147
+ </Form>
148
+
149
+ <div className={styles.actions}>
150
+ <Button onClick={handleReset}>Reset</Button>
151
+ <Button type="primary" onClick={handleSave}>
152
+ Save & Copy
153
+ </Button>
154
+ </div>
155
+ </div>
156
+ );
157
+ };
158
+
159
+ export default OptionsEditor;
160
+
@@ -0,0 +1,41 @@
1
+ export default {
2
+ theme: {
3
+ colorPrimary: '#615CED',
4
+ darkMode: true,
5
+ prefix: 'agentscope-runtime-webui',
6
+ leftHeader: {
7
+ logo: 'https://img.alicdn.com/imgextra/i2/O1CN01lmoGYn1kjoXATy4PX_!!6000000004720-2-tps-200-200.png',
8
+ title: 'Runtime WebUI',
9
+ },
10
+ },
11
+ sender: {
12
+ attachments: false,
13
+ maxLength: 10000,
14
+ disclaimer:
15
+ 'AI can also make mistakes, so please check carefully and use it with caution',
16
+ },
17
+
18
+ welcome: {
19
+ greeting: 'Hello, how can I help you today?',
20
+ description:
21
+ 'I am a helpful assistant that can help you with your questions.',
22
+ avatar:
23
+ 'https://img.alicdn.com/imgextra/i2/O1CN01lmoGYn1kjoXATy4PX_!!6000000004720-2-tps-200-200.png',
24
+ prompts: [
25
+ {
26
+ value: 'Hello',
27
+ },
28
+ {
29
+ value: 'How are you?',
30
+ },
31
+ {
32
+ value: 'What can you do?',
33
+ },
34
+ ],
35
+ },
36
+ api: {
37
+ baseURL: BASE_URL,
38
+ token: TOKEN,
39
+ },
40
+ };
41
+
@@ -0,0 +1,27 @@
1
+ import { SparkSettingLine } from "@agentscope-ai/icons";
2
+ import { IconButton, Drawer } from "@agentscope-ai/design";
3
+ import { useState } from "react";
4
+ import OptionsEditor from "./OptionsEditor";
5
+
6
+ interface OptionsPanelProps {
7
+ value?: any;
8
+ onChange?: any;
9
+ }
10
+
11
+ export default function OptionsPanel(props: OptionsPanelProps) {
12
+ const [open, setOpen] = useState(false);
13
+
14
+ return <>
15
+ <IconButton onClick={() => setOpen(true)} icon={<SparkSettingLine />} bordered={false} />
16
+ <Drawer
17
+ destroyOnHidden
18
+ open={open}
19
+ onClose={() => setOpen(false)}
20
+ styles={{ body: { padding: 0 }, header: { padding: 8 } }}>
21
+ <OptionsEditor value={props.value} onChange={(v: typeof props.value) => {
22
+ setOpen(false);
23
+ props.onChange(v);
24
+ }} />
25
+ </Drawer>
26
+ </>
27
+ }
@@ -0,0 +1,45 @@
1
+ import { AgentScopeRuntimeWebUI, IAgentScopeRuntimeWebUIOptions } from '@agentscope-ai/chat';
2
+ import OptionsPanel from './OptionsPanel';
3
+ import { useMemo } from 'react';
4
+ import sessionApi from './sessionApi';
5
+ import { useLocalStorageState } from 'ahooks';
6
+ import defaultConfig from './OptionsPanel/defaultConfig';
7
+
8
+ export default function () {
9
+ const [optionsConfig, setOptionsConfig] = useLocalStorageState('agent-scope-runtime-webui-options', {
10
+ defaultValue: defaultConfig,
11
+ listenStorageChange: true,
12
+ });
13
+
14
+ const options = useMemo(() => {
15
+ const rightHeader = <OptionsPanel value={optionsConfig} onChange={(v: typeof optionsConfig) => {
16
+ setOptionsConfig(prev => ({
17
+ ...prev,
18
+ ...v,
19
+ }));
20
+ }} />;
21
+
22
+
23
+
24
+ return {
25
+ ...optionsConfig,
26
+ session: {
27
+ multiple: true,
28
+ api: sessionApi,
29
+ },
30
+ theme: {
31
+ ...optionsConfig.theme,
32
+ rightHeader,
33
+ },
34
+ };
35
+ }, [optionsConfig]);
36
+
37
+
38
+
39
+
40
+ return <div style={{ height: '100vh' }}>
41
+ <AgentScopeRuntimeWebUI
42
+ options={options as unknown as IAgentScopeRuntimeWebUIOptions}
43
+ />
44
+ </div>;
45
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ IAgentScopeRuntimeWebUISession,
3
+ IAgentScopeRuntimeWebUISessionAPI,
4
+ } from '@agentscope-ai/chat';
5
+
6
+ class SessionApi implements IAgentScopeRuntimeWebUISessionAPI {
7
+ private lsKey: string;
8
+ private sessionList: IAgentScopeRuntimeWebUISession[];
9
+
10
+ constructor() {
11
+ this.lsKey = 'agent-scope-runtime-webui-sessions';
12
+ this.sessionList = [];
13
+ }
14
+
15
+ async getSessionList() {
16
+ this.sessionList = JSON.parse(localStorage.getItem(this.lsKey) || '[]');
17
+ return [...this.sessionList];
18
+ }
19
+
20
+ async getSession(sessionId: string) {
21
+ return this.sessionList.find((session) => session.id === sessionId) as IAgentScopeRuntimeWebUISession;
22
+ }
23
+
24
+ async updateSession(session: Partial<IAgentScopeRuntimeWebUISession>) {
25
+ const index = this.sessionList.findIndex((item) => item.id === session.id);
26
+ if (index > -1) {
27
+ this.sessionList[index] = {
28
+ ...this.sessionList[index],
29
+ ...session,
30
+ };
31
+ localStorage.setItem(this.lsKey, JSON.stringify(this.sessionList));
32
+ }
33
+
34
+ return [...this.sessionList];
35
+ }
36
+
37
+ async createSession(session: Partial<IAgentScopeRuntimeWebUISession>) {
38
+ session.id = Date.now().toString();
39
+ this.sessionList.unshift(session as IAgentScopeRuntimeWebUISession);
40
+ localStorage.setItem(this.lsKey, JSON.stringify(this.sessionList));
41
+ return [...this.sessionList];
42
+ }
43
+
44
+ async removeSession(session: Partial<IAgentScopeRuntimeWebUISession>) {
45
+ this.sessionList = this.sessionList.filter(
46
+ (item) => item.id !== session.id,
47
+ );
48
+ localStorage.setItem(this.lsKey, JSON.stringify(this.sessionList));
49
+ return [...this.sessionList];
50
+ }
51
+ }
52
+
53
+ export default new SessionApi();
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App.tsx'
4
+
5
+ createRoot(document.getElementById('root')!).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>,
9
+ )
@@ -0,0 +1,4 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ declare const BASE_URL: string;
4
+ declare const TOKEN: string;
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "moduleDetection": "force",
13
+ "noEmit": true,
14
+ "jsx": "react-jsx",
15
+
16
+ "strict": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "erasableSyntaxOnly": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["src"]
24
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ { "path": "./tsconfig.app.json" },
5
+ { "path": "./tsconfig.node.json" }
6
+ ]
7
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ "moduleResolution": "bundler",
10
+ "allowImportingTsExtensions": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "erasableSyntaxOnly": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "noUncheckedSideEffectImports": true
20
+ },
21
+ "include": ["vite.config.ts"]
22
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ define: {
6
+ BASE_URL: JSON.stringify(process.env.BASE_URL || ''),
7
+ TOKEN: JSON.stringify(process.env.TOKEN || ''),
8
+ MOBILE: false,
9
+ },
10
+ plugins: [react()],
11
+ })
@@ -178,14 +178,15 @@ function Attachments(props: AttachmentsProps, ref: React.Ref<AttachmentsRef>) {
178
178
  };
179
179
 
180
180
  const onItemReplace = useEvent((oldItem: Attachment, file: File) => {
181
+ const { customRequest } = uploadProps;
181
182
  const newAttachment: Attachment = {
182
183
  uid: oldItem.uid,
183
184
  name: file.name,
184
185
  size: file.size,
185
186
  type: file.type,
186
187
  originFileObj: file as any,
187
- status: 'done',
188
- percent: 100,
188
+ status: customRequest ? 'uploading' : 'done',
189
+ percent: customRequest ? 0 : 100,
189
190
  };
190
191
  const newFileList = fileList.map((fileItem) =>
191
192
  fileItem.uid === oldItem.uid ? newAttachment : fileItem,
@@ -194,6 +195,33 @@ function Attachments(props: AttachmentsProps, ref: React.Ref<AttachmentsRef>) {
194
195
  file: newAttachment,
195
196
  fileList: newFileList,
196
197
  });
198
+
199
+ if (customRequest) {
200
+ const updateFile = (updates: Partial<Attachment>) => {
201
+ setFileList((prev) => {
202
+ const updated = prev.map((f) =>
203
+ f.uid === oldItem.uid ? { ...f, ...updates } : f,
204
+ );
205
+ onChange?.({ file: { ...newAttachment, ...updates }, fileList: updated });
206
+ return updated;
207
+ });
208
+ };
209
+
210
+ customRequest({
211
+ file: file as any,
212
+ onSuccess: (response: any) => {
213
+ updateFile({ status: 'done', percent: 100, response });
214
+ },
215
+ onError: (error: any) => {
216
+ updateFile({ status: 'error', error });
217
+ },
218
+ onProgress: (event: any) => {
219
+ updateFile({ percent: event?.percent });
220
+ },
221
+ } as any, {
222
+ defaultRequest: () => { }
223
+ });
224
+ }
197
225
  });
198
226
 
199
227
  let renderChildren: React.ReactElement;
@@ -69,6 +69,10 @@ export default forwardRef(function (_, ref) {
69
69
  setContent(content);
70
70
  setAttachedFiles(fileList || [[]]);
71
71
  },
72
+ clearInput: () => {
73
+ setContent('');
74
+ setAttachedFiles(attachedFilesRef.current.map(() => []));
75
+ },
72
76
  getAttachedFiles: () => attachedFilesRef.current,
73
77
 
74
78
  };
@@ -166,6 +170,7 @@ export default forwardRef(function (_, ref) {
166
170
  key={index}
167
171
  items={files}
168
172
  replaceable={true}
173
+ customRequest={onUpload[index]?.customRequest}
169
174
  onChange={(info) => handleFileChange(index, info.fileList)}
170
175
  />
171
176
  })
@@ -220,6 +220,7 @@ export type ChatAnywhereRef =
220
220
  ReturnType<typeof useSessionList> &
221
221
  {
222
222
  setInputContent: (content: string, fileList?: UploadFile[][]) => void;
223
+ clearInput: () => void;
223
224
  scrollToBottom: (options?: ScrollToBottomOptions) => void;
224
225
  reload: () => void;
225
226
  };
@@ -318,6 +318,11 @@ export interface IChatAnywhereRef extends IChatAnywhereContext {
318
318
  * @descriptionEn Method to set input field content
319
319
  */
320
320
  setInputContent: (content: string, fileList?: UploadFile[][]) => void;
321
+ /**
322
+ * @description 清空输入框内容和附件
323
+ * @descriptionEn Clear input content and attached files
324
+ */
325
+ clearInput: () => void;
321
326
  /**
322
327
  * @description 滚动到底部的方法
323
328
  * @descriptionEn Method to scroll to bottom
@@ -58,18 +58,35 @@ export interface IRagProps {
58
58
  filters?: string;
59
59
  }
60
60
 
61
- function Images({ images }: { images: string[] }) {
61
+ // 兼容上游传入的 images 类型不规范(如单个字符串、null 等),统一归一化为字符串数组
62
+ function normalizeImages(images?: string[] | string | null): string[] {
63
+ if (Array.isArray(images)) {
64
+ return images.filter((image): image is string => typeof image === 'string' && !!image);
65
+ }
66
+ if (typeof images === 'string' && images) {
67
+ return [images];
68
+ }
69
+ return [];
70
+ }
71
+
72
+ function Images({ images }: { images?: string[] | string | null }) {
62
73
  const { getPrefixCls } = useProviderContext();
63
74
 
64
75
  const prefixCls = getPrefixCls('operate-card');
65
76
 
77
+ const imageList = normalizeImages(images);
78
+
79
+ if (!imageList.length) {
80
+ return null;
81
+ }
82
+
66
83
  return <ConfigProvider
67
84
  locale={{
68
85
  Image: { preview: '' }
69
86
  } as Locale}
70
87
  >
71
88
  <Image.PreviewGroup>
72
- {images.map((image, index) => <Image src={image} key={index} width={44} height={44} />)}
89
+ {imageList.map((image, index) => <Image src={image} key={index} width={44} height={44} />)}
73
90
  </Image.PreviewGroup>
74
91
  </ConfigProvider>
75
92
 
@@ -106,10 +123,10 @@ function Item({ item }) {
106
123
  <div className={`${prefixCls}-rag-item-content-text`}>{item.content}</div>
107
124
 
108
125
  {
109
- item.images &&
126
+ normalizeImages(item.images).length ?
110
127
  <div className={`${prefixCls}-rag-item-images`}>
111
128
  <Images images={item.images} />
112
- </div>
129
+ </div> : null
113
130
  }
114
131
 
115
132
  {
@@ -146,7 +163,7 @@ export default function (props: IRagProps) {
146
163
  </div>
147
164
 
148
165
  {
149
- images?.length ? <div>
166
+ normalizeImages(images).length ? <div>
150
167
  <div className={`${prefixCls}-rag-group-title`}>检索图片</div>
151
168
  <div className={`${prefixCls}-rag-group-content ${prefixCls}-rag-group-content-images`}>
152
169
  <Images images={images} />
@@ -97,14 +97,15 @@ function Attachments(props, ref) {
97
97
  });
98
98
  };
99
99
  var onItemReplace = useEvent(function (oldItem, file) {
100
+ var customRequest = uploadProps.customRequest;
100
101
  var newAttachment = {
101
102
  uid: oldItem.uid,
102
103
  name: file.name,
103
104
  size: file.size,
104
105
  type: file.type,
105
106
  originFileObj: file,
106
- status: 'done',
107
- percent: 100
107
+ status: customRequest ? 'uploading' : 'done',
108
+ percent: customRequest ? 0 : 100
108
109
  };
109
110
  var newFileList = fileList.map(function (fileItem) {
110
111
  return fileItem.uid === oldItem.uid ? newAttachment : fileItem;
@@ -113,6 +114,43 @@ function Attachments(props, ref) {
113
114
  file: newAttachment,
114
115
  fileList: newFileList
115
116
  });
117
+ if (customRequest) {
118
+ var updateFile = function updateFile(updates) {
119
+ setFileList(function (prev) {
120
+ var updated = prev.map(function (f) {
121
+ return f.uid === oldItem.uid ? _objectSpread(_objectSpread({}, f), updates) : f;
122
+ });
123
+ onChange === null || onChange === void 0 || onChange({
124
+ file: _objectSpread(_objectSpread({}, newAttachment), updates),
125
+ fileList: updated
126
+ });
127
+ return updated;
128
+ });
129
+ };
130
+ customRequest({
131
+ file: file,
132
+ onSuccess: function onSuccess(response) {
133
+ updateFile({
134
+ status: 'done',
135
+ percent: 100,
136
+ response: response
137
+ });
138
+ },
139
+ onError: function onError(error) {
140
+ updateFile({
141
+ status: 'error',
142
+ error: error
143
+ });
144
+ },
145
+ onProgress: function onProgress(event) {
146
+ updateFile({
147
+ percent: event === null || event === void 0 ? void 0 : event.percent
148
+ });
149
+ }
150
+ }, {
151
+ defaultRequest: function defaultRequest() {}
152
+ });
153
+ }
116
154
  });
117
155
  var renderChildren;
118
156
  var getPlaceholderNode = function getPlaceholderNode(type, props, ref) {
@@ -100,6 +100,12 @@ export default /*#__PURE__*/forwardRef(function (_, ref) {
100
100
  setContent(content);
101
101
  setAttachedFiles(fileList || [[]]);
102
102
  },
103
+ clearInput: function clearInput() {
104
+ setContent('');
105
+ setAttachedFiles(attachedFilesRef.current.map(function () {
106
+ return [];
107
+ }));
108
+ },
103
109
  getAttachedFiles: function getAttachedFiles() {
104
110
  return attachedFilesRef.current;
105
111
  }
@@ -197,10 +203,12 @@ export default /*#__PURE__*/forwardRef(function (_, ref) {
197
203
  return item.length;
198
204
  }),
199
205
  children: attachedFiles.map(function (files, index) {
206
+ var _onUpload$index;
200
207
  if (!files.length) return null;
201
208
  return /*#__PURE__*/_jsx(Attachments, {
202
209
  items: files,
203
210
  replaceable: true,
211
+ customRequest: (_onUpload$index = onUpload[index]) === null || _onUpload$index === void 0 ? void 0 : _onUpload$index.customRequest,
204
212
  onChange: function onChange(info) {
205
213
  return handleFileChange(index, info.fileList);
206
214
  }
@@ -149,6 +149,7 @@ export interface IChatAnywhereContext {
149
149
  }
150
150
  export type ChatAnywhereRef = ReturnType<typeof useMessages> & ReturnType<typeof useInput> & ReturnType<typeof useSessionList> & {
151
151
  setInputContent: (content: string, fileList?: UploadFile[][]) => void;
152
+ clearInput: () => void;
152
153
  scrollToBottom: (options?: ScrollToBottomOptions) => void;
153
154
  reload: () => void;
154
155
  };
@@ -316,6 +316,11 @@ export interface IChatAnywhereRef extends IChatAnywhereContext {
316
316
  * @descriptionEn Method to set input field content
317
317
  */
318
318
  setInputContent: (content: string, fileList?: UploadFile[][]) => void;
319
+ /**
320
+ * @description 清空输入框内容和附件
321
+ * @descriptionEn Clear input content and attached files
322
+ */
323
+ clearInput: () => void;
319
324
  /**
320
325
  * @description 滚动到底部的方法
321
326
  * @descriptionEn Method to scroll to bottom
@@ -13,11 +13,27 @@ import { useState } from 'react';
13
13
  import { jsx as _jsx } from "react/jsx-runtime";
14
14
  import { jsxs as _jsxs } from "react/jsx-runtime";
15
15
  import { Fragment as _Fragment } from "react/jsx-runtime";
16
+ // 兼容上游传入的 images 类型不规范(如单个字符串、null 等),统一归一化为字符串数组
17
+ function normalizeImages(images) {
18
+ if (Array.isArray(images)) {
19
+ return images.filter(function (image) {
20
+ return typeof image === 'string' && !!image;
21
+ });
22
+ }
23
+ if (typeof images === 'string' && images) {
24
+ return [images];
25
+ }
26
+ return [];
27
+ }
16
28
  function Images(_ref) {
17
29
  var images = _ref.images;
18
30
  var _useProviderContext = useProviderContext(),
19
31
  getPrefixCls = _useProviderContext.getPrefixCls;
20
32
  var prefixCls = getPrefixCls('operate-card');
33
+ var imageList = normalizeImages(images);
34
+ if (!imageList.length) {
35
+ return null;
36
+ }
21
37
  return /*#__PURE__*/_jsx(ConfigProvider, {
22
38
  locale: {
23
39
  Image: {
@@ -25,7 +41,7 @@ function Images(_ref) {
25
41
  }
26
42
  },
27
43
  children: /*#__PURE__*/_jsx(Image.PreviewGroup, {
28
- children: images.map(function (image, index) {
44
+ children: imageList.map(function (image, index) {
29
45
  return /*#__PURE__*/_jsx(Image, {
30
46
  src: image,
31
47
  width: 44,
@@ -72,12 +88,12 @@ function Item(_ref2) {
72
88
  children: [/*#__PURE__*/_jsx("div", {
73
89
  className: "".concat(prefixCls, "-rag-item-content-text"),
74
90
  children: item.content
75
- }), item.images && /*#__PURE__*/_jsx("div", {
91
+ }), normalizeImages(item.images).length ? /*#__PURE__*/_jsx("div", {
76
92
  className: "".concat(prefixCls, "-rag-item-images"),
77
93
  children: /*#__PURE__*/_jsx(Images, {
78
94
  images: item.images
79
95
  })
80
- }), item.link ? /*#__PURE__*/_jsx("a", {
96
+ }) : null, item.link ? /*#__PURE__*/_jsx("a", {
81
97
  onClick: function onClick() {
82
98
  window.open(item.link, '_blank');
83
99
  },
@@ -115,7 +131,7 @@ export default function (props) {
115
131
  className: "".concat(prefixCls, "-rag-group-content"),
116
132
  children: query
117
133
  })]
118
- }), images !== null && images !== void 0 && images.length ? /*#__PURE__*/_jsxs("div", {
134
+ }), normalizeImages(images).length ? /*#__PURE__*/_jsxs("div", {
119
135
  children: [/*#__PURE__*/_jsx("div", {
120
136
  className: "".concat(prefixCls, "-rag-group-title"),
121
137
  children: "\u68C0\u7D22\u56FE\u7247"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentscope-ai/chat",
3
- "version": "1.1.68",
3
+ "version": "1.1.70",
4
4
  "description": "a free and open-source chat framework for building excellent LLM-powered chat experiences",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": [