9
9
useXAgent ,
10
10
useXChat
11
11
} from "@ant-design/x" ;
12
- import { createStyles } from "antd-style" ;
13
12
import React , { useEffect } from "react" ;
14
13
import {
15
14
CloudUploadOutlined ,
@@ -25,9 +24,23 @@ import {
25
24
EditOutlined ,
26
25
ShareAltOutlined
27
26
} from "@ant-design/icons" ;
28
- import { Flex , App , Badge , Button , type GetProp , Space , theme } from "antd" ;
27
+ import {
28
+ Flex ,
29
+ App ,
30
+ Badge ,
31
+ Button ,
32
+ Space ,
33
+ Typography ,
34
+ Tag ,
35
+ type GetProp ,
36
+ Tooltip ,
37
+ Select
38
+ } from "antd" ;
29
39
import ReactMarkdown from "react-markdown" ;
30
- import { getChat } from "./request" ;
40
+ import { getChat , getModels } from "./request" ;
41
+ import { useStyle } from "./style" ;
42
+
43
+ const DEFAULT_MODEL = "qwen-plus" ;
31
44
32
45
const decoder = new TextDecoder ( "utf-8" ) ;
33
46
@@ -38,9 +51,7 @@ const renderTitle = (icon: React.ReactElement, title: string) => (
38
51
</ Space >
39
52
) ;
40
53
41
- // 用于临时保存会话记录
42
- const messagesMap = { } as Record < string , Array < any > > ;
43
-
54
+ // 新会话默认展示
44
55
const placeholderPromptsItems : GetProp < typeof Prompts , "items" > = [
45
56
{
46
57
key : "1" ,
@@ -88,92 +99,24 @@ const placeholderPromptsItems: GetProp<typeof Prompts, "items"> = [
88
99
}
89
100
] ;
90
101
102
+ // 默认会话
91
103
const defaultKey = Date . now ( ) . toString ( ) ;
92
-
93
104
const defaultConversationsItems = [
94
105
{
95
106
key : defaultKey ,
96
- label : "What is Spring Ai Alibaba?"
107
+ label : (
108
+ < span >
109
+ Conversation 1
110
+ < Tag style = { { marginLeft : 8 } } color = "green" >
111
+ { DEFAULT_MODEL }
112
+ </ Tag >
113
+ </ span >
114
+ )
97
115
}
98
116
] ;
99
117
100
- const useStyle = createStyles ( ( { token, css } ) => {
101
- return {
102
- layout : css `
103
- width : 100% ;
104
- min-width : 1000px ;
105
- height : 722px ;
106
- border-radius : ${ token . borderRadius } px;
107
- display : flex;
108
- background : ${ token . colorBgContainer } ;
109
- font-family : AlibabaPuHuiTi, ${ token . fontFamily } , sans-serif;
110
-
111
- .ant-prompts {
112
- color : ${ token . colorText } ;
113
- }
114
- ` ,
115
- menu : css `
116
- background : ${ token . colorBgLayout } 80 ;
117
- width : 280px ;
118
- height : 100% ;
119
- display : flex;
120
- flex-direction : column;
121
- ` ,
122
- conversations : css `
123
- padding : 0 12px ;
124
- flex : 1 ;
125
- overflow-y : auto;
126
- ` ,
127
- chat : css `
128
- height : 100% ;
129
- width : 100% ;
130
- max-width : 700px ;
131
- margin : 0 auto;
132
- box-sizing : border-box;
133
- display : flex;
134
- flex-direction : column;
135
- padding : ${ token . paddingLG } px;
136
- gap : 16px ;
137
- ` ,
138
- messages : css `
139
- flex : 1 ;
140
- ` ,
141
- placeholder : css `
142
- padding-top : 32px ;
143
- ` ,
144
- sender : css `
145
- box-shadow : ${ token . boxShadow } ;
146
- ` ,
147
- logo : css `
148
- display : flex;
149
- height : 72px ;
150
- align-items : center;
151
- justify-content : start;
152
- padding : 0 24px ;
153
- box-sizing : border-box;
154
-
155
- img {
156
- width : 24px ;
157
- height : 24px ;
158
- display : inline-block;
159
- }
160
-
161
- span {
162
- display : inline-block;
163
- margin : 0 8px ;
164
- font-weight : bold;
165
- color : ${ token . colorText } ;
166
- font-size : 16px ;
167
- }
168
- ` ,
169
- addBtn : css `
170
- background : # 1677ff0f ;
171
- border : 1px solid # 1677ff34 ;
172
- width : calc (100% - 24px );
173
- margin : 0 12px 24px 12px ;
174
- `
175
- } ;
176
- } ) ;
118
+ // 用于临时保存会话记录
119
+ const messagesMap = { } as Record < string , { model : string ; messages : any [ ] } > ;
177
120
178
121
const senderPromptsItems : GetProp < typeof Prompts , "items" > = [
179
122
{
@@ -188,6 +131,7 @@ const senderPromptsItems: GetProp<typeof Prompts, "items"> = [
188
131
}
189
132
] ;
190
133
134
+ // 会话中角色列表
191
135
const roles : GetProp < typeof Bubble . List , "roles" > = {
192
136
ai : {
193
137
placement : "start" ,
@@ -196,7 +140,25 @@ const roles: GetProp<typeof Bubble.List, "roles"> = {
196
140
content : {
197
141
borderRadius : 16
198
142
}
199
- }
143
+ } ,
144
+ messageRender : ( content ) => (
145
+ < Typography >
146
+ < ReactMarkdown > { content } </ ReactMarkdown >
147
+ </ Typography >
148
+ )
149
+ } ,
150
+ aiHistory : {
151
+ placement : "start" ,
152
+ styles : {
153
+ content : {
154
+ borderRadius : 16
155
+ }
156
+ } ,
157
+ messageRender : ( content ) => (
158
+ < Typography >
159
+ < ReactMarkdown > { content } </ ReactMarkdown >
160
+ </ Typography >
161
+ )
200
162
} ,
201
163
local : {
202
164
placement : "end" ,
@@ -237,13 +199,17 @@ const Independent: React.FC = () => {
237
199
> ( [ ] ) ;
238
200
239
201
const { message } = App . useApp ( ) ;
240
- const [ recording , setRecording ] = React . useState ( false ) ;
241
- const { token } = theme . useToken ( ) ;
202
+
203
+ // 当前会话的模型
204
+ const [ model , setModel ] = React . useState ( DEFAULT_MODEL ) ;
205
+ // 将要新增会话的模型
206
+ const [ nextModel , setNextModel ] = React . useState ( DEFAULT_MODEL ) ;
242
207
243
208
// ==================== Runtime ====================
244
209
const [ agent ] = useXAgent ( {
245
- request : async ( { message } , { onSuccess } ) => {
210
+ request : async ( { message } , { onSuccess, onUpdate } ) => {
246
211
let buffer = "" ;
212
+ onUpdate ( JSON . stringify ( { role : "ai" , value : "" } ) ) ;
247
213
248
214
const res = await getChat (
249
215
JSON . parse ( message || "{}" ) ?. value || "" ,
@@ -257,13 +223,15 @@ const Independent: React.FC = () => {
257
223
res . forEach ( ( item ) => {
258
224
if ( item ?. message === "success" ) {
259
225
buffer = buffer + item ?. data ;
226
+ onUpdate ( JSON . stringify ( { role : "ai" , value : buffer } ) ) ;
260
227
}
261
228
} ) ;
262
229
}
263
230
} ,
264
231
{
265
232
image : attachedFiles ?. [ 0 ] ?. originFileObj ,
266
- chatId : activeKey
233
+ chatId : activeKey ,
234
+ model
267
235
}
268
236
) ;
269
237
@@ -274,11 +242,29 @@ const Independent: React.FC = () => {
274
242
value =
275
243
"Request failed." + ( res ?. statusText ? " " + res ?. statusText : "" ) ;
276
244
}
245
+
277
246
onSuccess ( JSON . stringify ( { role : "ai" , value } ) ) ;
278
247
} ,
279
248
customParams : [ attachedFiles ]
280
249
} ) ;
281
250
251
+ // 获取模型列表
252
+ const [ modelItems , setModelItems ] = React . useState ( [ ] ) ;
253
+ useEffect ( ( ) => {
254
+ getModels ( ) . then ( ( res ) => {
255
+ setModelItems (
256
+ res . map ( ( { model, desc } ) => ( {
257
+ value : model ,
258
+ label : (
259
+ < Tooltip title = { desc } placement = "right" >
260
+ { model }
261
+ </ Tooltip >
262
+ )
263
+ } ) )
264
+ ) ;
265
+ } ) ;
266
+ } , [ ] ) ;
267
+
282
268
const [ items , setItems ] = React . useState <
283
269
GetProp < typeof Bubble . List , "items" >
284
270
> ( [ ] ) ;
@@ -322,30 +308,61 @@ const Independent: React.FC = () => {
322
308
onRequest ( info . data . description as string ) ;
323
309
} ;
324
310
311
+ // 将模型返回的消息的 role 转换成历史记录,避免切换会话触发渲染动效
312
+ const getMessageHistory = ( ) => {
313
+ return messages . map ( ( item ) => {
314
+ const value = JSON . parse ( item . message ) ;
315
+ if ( value . role === "ai" ) {
316
+ value . role = "aiHistory" ;
317
+ item . message = JSON . stringify ( value ) ;
318
+ return item ;
319
+ } else {
320
+ return item ;
321
+ }
322
+ } ) ;
323
+ } ;
324
+
325
+ // 新增会话
325
326
const onAddConversation = async ( ) => {
326
327
const newKey = Date . now ( ) . toString ( ) ;
327
328
setConversationsItems ( [
328
329
...conversationsItems ,
329
330
{
330
331
key : newKey ,
331
- label : `New Conversation ${ conversationsItems . length } `
332
+ label : (
333
+ < span >
334
+ { `Conversation ${ conversationsItems . length + 1 } ` }
335
+ < Tag style = { { marginLeft : 8 } } color = "green" >
336
+ { nextModel }
337
+ </ Tag >
338
+ </ span >
339
+ )
332
340
}
333
341
] ) ;
334
- messagesMap [ activeKey ] = messages ;
342
+ messagesMap [ activeKey ] = {
343
+ model,
344
+ messages : getMessageHistory ( )
345
+ } ;
335
346
setHeaderOpen ( false ) ;
336
347
setAttachedFiles ( [ ] ) ;
337
- setMessages ( [ ] ) ;
338
348
setActiveKey ( newKey ) ;
349
+ setMessages ( [ ] ) ;
350
+ setModel ( nextModel ) ;
339
351
} ;
340
352
353
+ // 切换会话
341
354
const onConversationClick : GetProp < typeof Conversations , "onActiveChange" > = (
342
355
key
343
356
) => {
344
- messagesMap [ activeKey ] = messages ;
357
+ messagesMap [ activeKey ] = {
358
+ model,
359
+ messages : getMessageHistory ( )
360
+ } ;
345
361
setHeaderOpen ( false ) ;
346
362
setAttachedFiles ( [ ] ) ;
347
- setMessages ( messagesMap [ key ] || [ ] ) ;
348
363
setActiveKey ( key ) ;
364
+ setMessages ( messagesMap [ key ] . messages || [ ] ) ;
365
+ setModel ( messagesMap [ key ] . model || DEFAULT_MODEL ) ;
349
366
} ;
350
367
351
368
const handleFileChange : GetProp < typeof Attachments , "onChange" > = ( info ) => {
@@ -402,6 +419,7 @@ const Independent: React.FC = () => {
402
419
</ Space >
403
420
) ;
404
421
422
+ // messages 转 items
405
423
useEffect ( ( ) => {
406
424
setItems (
407
425
messages . map ( ( { id, message, status } ) => {
@@ -410,8 +428,8 @@ const Independent: React.FC = () => {
410
428
const value = item ?. value ;
411
429
return {
412
430
key : id ,
413
- loading : status === "loading" ,
414
- role : "file" ,
431
+ role : item ?. role ,
432
+ loading : ! value ,
415
433
content : [
416
434
{
417
435
uid : value ?. uid ,
@@ -421,11 +439,12 @@ const Independent: React.FC = () => {
421
439
]
422
440
} ;
423
441
} else {
442
+ const value = item ?. value ;
424
443
return {
425
444
key : id ,
426
- loading : status === "loading" ,
427
- role : status === "local" ? "local" : "ai" ,
428
- content : < ReactMarkdown > { item . value } </ ReactMarkdown >
445
+ role : item ?. role ,
446
+ loading : ! value ,
447
+ content : value
429
448
} ;
430
449
}
431
450
} )
@@ -488,6 +507,16 @@ const Independent: React.FC = () => {
488
507
< div className = { styles . menu } >
489
508
{ /* 🌟 Logo */ }
490
509
{ logoNode }
510
+ { /* 🌟 模型选择 */ }
511
+ < div className = { styles . chooseModel } >
512
+ 选择模型类型
513
+ < Select
514
+ onChange = { setNextModel }
515
+ options = { modelItems }
516
+ style = { { width : 120 } }
517
+ value = { nextModel }
518
+ />
519
+ </ div >
491
520
{ /* 🌟 添加会话 */ }
492
521
< Button
493
522
onClick = { onAddConversation }
@@ -524,14 +553,7 @@ const Independent: React.FC = () => {
524
553
value = { content }
525
554
header = { senderHeader }
526
555
onSubmit = { onSubmit }
527
- allowSpeech = { {
528
- // When setting `recording`, the built-in speech recognition feature will be disabled
529
- recording,
530
- onRecordingChange : ( nextRecording ) => {
531
- message . info ( `Mock Customize Recording: ${ nextRecording } ` ) ;
532
- setRecording ( nextRecording ) ;
533
- }
534
- } }
556
+ allowSpeech
535
557
onChange = { setContent }
536
558
prefix = { attachmentsNode }
537
559
loading = { agent . isRequesting ( ) }
0 commit comments