Duibonduil commited on
Commit
4cf82c9
·
verified ·
1 Parent(s): 2bae731

Upload 3 files

Browse files
aworld/cmd/web/webui/src/pages/App.tsx ADDED
@@ -0,0 +1,743 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ AlertFilled,
3
+ CloudUploadOutlined,
4
+ CopyOutlined,
5
+ DeleteOutlined,
6
+ MenuUnfoldOutlined,
7
+ ShrinkOutlined,
8
+ BoxPlotOutlined,
9
+ PaperClipOutlined,
10
+ PlusOutlined,
11
+ QuestionCircleOutlined,
12
+ ReloadOutlined
13
+ } from '@ant-design/icons';
14
+ import BubbleItem from './components/BubbleItem';
15
+ import {
16
+ Attachments,
17
+ Bubble,
18
+ Conversations,
19
+ Sender,
20
+ useXAgent,
21
+ useXChat
22
+ } from '@ant-design/x';
23
+ import { Avatar, Button, Flex, type GetProp, message, Spin, Drawer } from 'antd';
24
+ import { createStyles } from 'antd-style';
25
+ import React, { useEffect, useRef, useState } from 'react';
26
+ import logo from '../assets/aworld_logo.png';
27
+ import { useAgentId } from '../hooks/useAgentId';
28
+ import { useSessionId } from '../hooks/useSessionId';
29
+ import Prompts from '../pages/components/Prompts';
30
+ import Welcome from '../pages/components/Welcome';
31
+ import Workspace from './components/Drawer/Workspace';
32
+ import Trace from '../pages/components/Drawer/Trace';
33
+ import './index.less';
34
+
35
+ type BubbleDataType = {
36
+ role: string;
37
+ content: string;
38
+ };
39
+
40
+ // 添加会话数据类型定义
41
+ type SessionMessage = {
42
+ role: string;
43
+ content: string;
44
+ };
45
+
46
+ type SessionData = {
47
+ user_id: string;
48
+ session_id: string;
49
+ name: string;
50
+ description: string;
51
+ created_at: string;
52
+ updated_at: string;
53
+ messages: SessionMessage[];
54
+ };
55
+
56
+ // 会话列表项类型
57
+ type ConversationItem = {
58
+ key: string;
59
+ label: string;
60
+ group: string;
61
+ };
62
+
63
+ const DEFAULT_CONVERSATIONS_ITEMS: ConversationItem[] = [];
64
+
65
+ const SENDER_PROMPTS: GetProp<typeof Prompts, 'items'> = [];
66
+
67
+ const useStyle = createStyles(({ token, css }) => {
68
+ return {
69
+ layout: css`
70
+ width: 100%;
71
+ min-width: 1000px;
72
+ height: 100vh;
73
+ display: flex;
74
+ background: ${token.colorBgContainer};
75
+ font-family: AlibabaPuHuiTi, ${token.fontFamily}, sans-serif;
76
+ `,
77
+ // sider 样式
78
+ sider: css`
79
+ background: ${token.colorBgLayout}80;
80
+ width: 280px;
81
+ height: 100%;
82
+ display: flex;
83
+ flex-direction: column;
84
+ padding: 0 12px;
85
+ box-sizing: border-box;
86
+ `,
87
+ logo: css`
88
+ display: flex;
89
+ align-items: center;
90
+ justify-content: start;
91
+ box-sizing: border-box;
92
+ gap: 8px;
93
+ margin: 24px 0;
94
+
95
+ span {
96
+ font-weight: bold;
97
+ color: ${token.colorText};
98
+ font-size: 16px;
99
+ }
100
+ `,
101
+ addBtn: css`
102
+ background: #1677ff0f;
103
+ border: 1px solid #1677ff34;
104
+ height: 40px;
105
+ `,
106
+ conversations: css`
107
+ flex: 1;
108
+ overflow-y: auto;
109
+ margin-top: 12px;
110
+ padding: 0;
111
+
112
+ .ant-conversations-list {
113
+ padding-inline-start: 0;
114
+ }
115
+ `,
116
+ siderFooter: css`
117
+ border-top: 1px solid ${token.colorBorderSecondary};
118
+ height: 40px;
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: space-between;
122
+ `,
123
+ // chat list 样式
124
+ chat: css`
125
+ height: 100%;
126
+ width: 100%;
127
+ box-sizing: border-box;
128
+ display: flex;
129
+ flex-direction: column;
130
+ padding-block: ${token.paddingLG}px;
131
+ gap: 16px;
132
+ `,
133
+ chatPrompt: css`
134
+ .ant-prompts-label {
135
+ color: #000000e0 !important;
136
+ }
137
+ .ant-prompts-desc {
138
+ color: #000000a6 !important;
139
+ width: 100%;
140
+ }
141
+ .ant-prompts-icon {
142
+ color: #000000a6 !important;
143
+ }
144
+ `,
145
+ chatList: css`
146
+ flex: 1;
147
+ overflow: auto;
148
+ `,
149
+ loadingMessage: css`
150
+ background-image: linear-gradient(90deg, #ff6b23 0%, #af3cb8 31%, #53b6ff 89%);
151
+ background-size: 100% 2px;
152
+ background-repeat: no-repeat;
153
+ background-position: bottom;
154
+ `,
155
+ placeholder: css`
156
+ width: 100%;
157
+ max-width: 700px;
158
+ margin: 0 auto;
159
+ height: 100%;
160
+ display: flex;
161
+ flex-direction: column;
162
+ justify-content: center;
163
+ `,
164
+ // sender 样式
165
+ sender: css`
166
+ width: 100%;
167
+ max-width: 700px;
168
+ margin: 0 auto;
169
+ `,
170
+ speechButton: css`
171
+ font-size: 18px;
172
+ color: ${token.colorText} !important;
173
+ `,
174
+ senderPrompt: css`
175
+ width: 100%;
176
+ max-width: 700px;
177
+ margin: 0 auto;
178
+ color: ${token.colorText};
179
+ `,
180
+ sendButton: css`
181
+ background-color: #000000 !important;
182
+ border: none !important;
183
+ transition: opacity 0.2s;
184
+
185
+ &:hover {
186
+ background-color: rgba(0, 0, 0, 0.7) !important;
187
+ }
188
+
189
+ &:disabled {
190
+ opacity: 0.5 !important;
191
+ cursor: not-allowed;
192
+ background-color: rgba(0, 0, 0, 0.1) !important;
193
+ }
194
+
195
+ &:disabled:hover,
196
+ &:disabled:focus {
197
+ opacity: 0.5 !important;
198
+ background-color: rgba(0, 0, 0, 0.1) !important;
199
+ }
200
+ `,
201
+ };
202
+ });
203
+
204
+ const App: React.FC = () => {
205
+ const { styles } = useStyle();
206
+ const abortController = useRef<AbortController>(null);
207
+ const { sessionId, generateNewSessionId, updateURLSessionId, setSessionId } = useSessionId();
208
+ const { agentId, setAgentIdAndUpdateURL } = useAgentId();
209
+
210
+ // ==================== State ====================
211
+ const [messageHistory, setMessageHistory] = useState<Record<string, any>>({});
212
+ const [sessionData, setSessionData] = useState<Record<string, SessionData>>({});
213
+
214
+ const [conversations, setConversations] = useState<ConversationItem[]>(DEFAULT_CONVERSATIONS_ITEMS);
215
+ const [curConversation, setCurConversation] = useState<string>('');
216
+
217
+ const [attachmentsOpen, setAttachmentsOpen] = useState(false);
218
+ const [attachedFiles, setAttachedFiles] = useState<GetProp<typeof Attachments, 'items'>>([]);
219
+
220
+ const [inputValue, setInputValue] = useState('');
221
+ const [models, setModels] = useState<Array<{ label: string; value: string }>>([]);
222
+ const [selectedModel, setSelectedModel] = useState<string>('');
223
+ const [modelsLoading, setModelsLoading] = useState(false);
224
+
225
+ // 右侧抽屉
226
+ type DrawerContentType = 'Team workspace' | 'trace';
227
+ const [drawerVisible, setDrawerVisible] = useState(false);
228
+ const [drawerContent, setDrawerContent] = useState<DrawerContentType>('Team workspace');
229
+ const openDrawer = (content: DrawerContentType) => {
230
+ setDrawerVisible(true);
231
+ setDrawerContent(content);
232
+ }
233
+ const closeDrawer = () =>{
234
+ setDrawerVisible(false);
235
+ }
236
+
237
+ // ==================== API Calls ====================
238
+ const fetchModels = async () => {
239
+ setModelsLoading(true);
240
+ try {
241
+ const response = await fetch('/api/agent/models');
242
+ if (response.ok) {
243
+ const data = await response.json();
244
+ const modelOptions = Object.values(data).map((model: any) => ({
245
+ label: model.name || model.id,
246
+ value: model.id
247
+ }));
248
+ setModels(modelOptions);
249
+ } else {
250
+ message.error('Failed to fetch models');
251
+ }
252
+ } catch (error) {
253
+ console.error('Error fetching models:', error);
254
+ message.error('Error fetching models');
255
+ } finally {
256
+ setModelsLoading(false);
257
+ }
258
+ };
259
+
260
+ const fetchSessions = async () => {
261
+ try {
262
+ const response = await fetch('/api/session/list');
263
+ if (response.ok) {
264
+ const sessions: SessionData[] = await response.json();
265
+
266
+ const sessionDataMap: Record<string, SessionData> = {};
267
+ sessions.forEach(session => {
268
+ sessionDataMap[session.session_id] = session;
269
+ });
270
+ setSessionData(sessionDataMap);
271
+
272
+ const conversationItems: ConversationItem[] = sessions.map(session => {
273
+ let label = session.name || session.description;
274
+ if (!label && session.messages.length > 0) {
275
+ const firstUserMessage = session.messages.find(msg => msg.role === 'user');
276
+ if (firstUserMessage) {
277
+ label = firstUserMessage.content.length > 50
278
+ ? firstUserMessage.content.substring(0, 50) + '...'
279
+ : firstUserMessage.content;
280
+ } else {
281
+ label = 'New Conversation';
282
+ }
283
+ }
284
+ if (!label) {
285
+ label = 'New Conversation';
286
+ }
287
+
288
+ return {
289
+ key: session.session_id,
290
+ label,
291
+ group: ''
292
+ };
293
+ });
294
+
295
+ setConversations(conversationItems);
296
+ } else {
297
+ console.error('Failed to fetch sessions');
298
+ }
299
+ } catch (error) {
300
+ console.error('Error fetching sessions:', error);
301
+ }
302
+ };
303
+
304
+ useEffect(() => {
305
+ fetchModels();
306
+ fetchSessions();
307
+ }, []);
308
+
309
+ useEffect(() => {
310
+ if (agentId && models.length > 0) {
311
+ const modelExists = models.find(model => model.value === agentId);
312
+ if (modelExists) {
313
+ setSelectedModel(agentId);
314
+ } else {
315
+ setAgentIdAndUpdateURL('');
316
+ }
317
+ }
318
+ }, [agentId, models, setAgentIdAndUpdateURL]);
319
+
320
+ const handleModelChange = (modelId: string) => {
321
+ setSelectedModel(modelId);
322
+ setAgentIdAndUpdateURL(modelId);
323
+ };
324
+
325
+ /**
326
+ * 🔔 Please replace the BASE_URL, PATH, MODEL, API_KEY with your own values.
327
+ */
328
+
329
+ // ==================== Runtime ====================
330
+ const [agent] = useXAgent<BubbleDataType>({
331
+ baseURL: '/api/agent/chat/completions',
332
+ model: selectedModel,
333
+ dangerouslyApiKey: 'Bearer sk-xxxxxxxxxxxxxxxxxxxx',
334
+ });
335
+ const loading = agent.isRequesting();
336
+
337
+ const { onRequest, messages, setMessages } = useXChat({
338
+ agent,
339
+ requestFallback: (_, { error }) => {
340
+ if (error.name === 'AbortError') {
341
+ return {
342
+ content: 'Request is aborted',
343
+ role: 'assistant',
344
+ };
345
+ }
346
+ return {
347
+ content: 'Request failed, please try again!',
348
+ role: 'assistant',
349
+ };
350
+ },
351
+ transformMessage: (info) => {
352
+ const { originMessage, chunk } = info || {};
353
+ let currentContent = '';
354
+ let currentThink = '';
355
+ try {
356
+ if (chunk?.data && !chunk?.data.includes('DONE')) {
357
+ const message = JSON.parse(chunk?.data);
358
+ currentThink = message?.choices?.[0]?.delta?.reasoning_content || '';
359
+ currentContent = message?.choices?.[0]?.delta?.content || '';
360
+ }
361
+ } catch (error) {
362
+ console.error(error);
363
+ }
364
+
365
+ let content = '';
366
+
367
+ if (!originMessage?.content && currentThink) {
368
+ content = `<think>${currentThink}`;
369
+ } else if (
370
+ originMessage?.content?.includes('<think>') &&
371
+ !originMessage?.content.includes('</think>') &&
372
+ currentContent
373
+ ) {
374
+ content = `${originMessage?.content}</think>${currentContent}`;
375
+ } else {
376
+ content = `${originMessage?.content || ''}${currentThink}${currentContent}`;
377
+ }
378
+ return {
379
+ content: content,
380
+ role: 'assistant',
381
+ };
382
+ },
383
+ resolveAbortController: (controller) => {
384
+ abortController.current = controller;
385
+ },
386
+ });
387
+
388
+ // ==================== Event ====================
389
+ const onSubmit = (val: string) => {
390
+ if (!val || !val.trim()) return;
391
+
392
+ if (loading) {
393
+ message.error('Request is in progress, please wait for the request to complete.');
394
+ return;
395
+ }
396
+
397
+ onRequest({
398
+ stream: true,
399
+ message: { role: 'user', content: val },
400
+ headers: {
401
+ 'X-Session-ID': sessionId,
402
+ },
403
+ });
404
+ };
405
+
406
+ // 复制消息内容到剪贴板
407
+ const copyMessageContent = async (content: string) => {
408
+ try {
409
+ await navigator.clipboard.writeText(content);
410
+ message.success('Message copied to clipboard');
411
+ } catch (error) {
412
+ console.error('Failed to copy message:', error);
413
+ message.error('Failed to copy message');
414
+ }
415
+ };
416
+
417
+ const resendMessage = (assistantMessage: any) => {
418
+ console.log('resendMessage: assistantMessage', assistantMessage, assistantMessage.messageIndex, assistantMessage.content, assistantMessage.message);
419
+ const assistantMessageIndex = assistantMessage.messageIndex;
420
+ const userMessageIndex = assistantMessageIndex - 1;
421
+ if (userMessageIndex >= 0 && messages[userMessageIndex]?.message?.role === 'user') {
422
+ const userMessage = messages[userMessageIndex].message.content;
423
+
424
+ // 删除当前assistant消息和对应的用户消息
425
+ const newMessages = messages.filter((_, index) => index !== assistantMessageIndex && index !== userMessageIndex);
426
+ setMessages(newMessages);
427
+
428
+ // 重新发送用户消息
429
+ setTimeout(() => {
430
+ onSubmit(userMessage);
431
+ }, 100);
432
+ } else {
433
+ message.error('Cannot find corresponding user message');
434
+ }
435
+ };
436
+
437
+ // ==================== Nodes ====================
438
+ const chatSider = (
439
+ <div className={styles.sider}>
440
+ {/* 🌟 Logo */}
441
+ <div className={styles.logo}>
442
+ <img src={logo} alt="AWorld Logo" width="24" height="24" />
443
+ <span>AWorld</span>
444
+ </div>
445
+
446
+ {/* 🌟 添加会话 */}
447
+ <Button
448
+ onClick={() => {
449
+ if (agent.isRequesting()) {
450
+ message.error(
451
+ 'Message is Requesting, you can create a new conversation after request done or abort it right now...',
452
+ );
453
+ return;
454
+ }
455
+
456
+ // 生成新的session ID
457
+ const newSessionId = generateNewSessionId();
458
+
459
+ // 创建新的会话项
460
+ const newConversation: ConversationItem = {
461
+ key: newSessionId,
462
+ label: `New Conversation`,
463
+ group: '', // 移除分组
464
+ };
465
+
466
+ setConversations([newConversation, ...conversations]);
467
+ setCurConversation(newSessionId);
468
+ setMessages([]);
469
+ }}
470
+ type="link"
471
+ className={styles.addBtn}
472
+ icon={<PlusOutlined />}
473
+ >
474
+ New Conversation
475
+ </Button>
476
+
477
+ <Conversations
478
+ items={conversations}
479
+ className={styles.conversations}
480
+ activeKey={curConversation}
481
+ onActiveChange={async (val) => {
482
+ console.log('active change: session_id', val);
483
+ setCurConversation(val);
484
+
485
+ setSessionId(val);
486
+ updateURLSessionId(val);
487
+
488
+ const session = sessionData[val];
489
+ if (session && session.messages.length > 0) {
490
+ const chatMessages = session.messages.map((msg, index) => ({
491
+ id: `${val}-${index}`,
492
+ message: {
493
+ role: msg.role,
494
+ content: msg.content
495
+ },
496
+ status: 'success' as const
497
+ }));
498
+ setMessages(chatMessages);
499
+ } else {
500
+ setMessages(messageHistory?.[val] || []);
501
+ }
502
+ }}
503
+ groupable={false}
504
+ styles={{ item: { padding: '0 8px' } }}
505
+ menu={(conversation) => ({
506
+ items: [
507
+ {
508
+ label: 'Delete',
509
+ key: 'delete',
510
+ icon: <DeleteOutlined />,
511
+ danger: true,
512
+ onClick: () => {
513
+ console.log('delete session: session_id', conversation.key);
514
+ fetch('/api/session/delete', {
515
+ method: 'POST',
516
+ headers: {
517
+ 'Content-Type': 'application/json',
518
+ },
519
+ body: JSON.stringify({ session_id: conversation.key }),
520
+ }).then((res) => res.json()).then((data) => {
521
+ if (data.code === 0) {
522
+ message.success('Session deleted');
523
+ fetchSessions();
524
+ } else {
525
+ message.error('Failed to delete session');
526
+ }
527
+ });
528
+ },
529
+ },
530
+ ],
531
+ })}
532
+ />
533
+
534
+ <div className={styles.siderFooter}>
535
+ <Avatar size={24} />
536
+ <Button type="text" icon={<QuestionCircleOutlined />} />
537
+ </div>
538
+ </div>
539
+ );
540
+ const chatList = (
541
+ <div className={styles.chatList}>
542
+ {messages?.length ? (
543
+ /* 🌟 消息列表 */
544
+ <Bubble.List
545
+ items={messages?.map((i, index) => ({
546
+ ...i.message,
547
+ content: (
548
+ <BubbleItem data={i.message.content || ''}/>
549
+ ),
550
+ classNames: {
551
+ content: i.status === 'loading' ? styles.loadingMessage : '',
552
+ },
553
+ typing: i.status === 'loading' ? { step: 5, interval: 20, suffix: <>💗</> } : false,
554
+ messageIndex: index,
555
+ }))}
556
+ style={{ height: '100%', paddingInline: 'calc(calc(100% - 700px) /2)' }}
557
+ roles={{
558
+ assistant: {
559
+ placement: 'start',
560
+ footer: (messageItem) => (
561
+ <div style={{ display: 'flex' }}>
562
+ <Button
563
+ type="text"
564
+ size="small"
565
+ icon={<ReloadOutlined />}
566
+ onClick={() => resendMessage(messageItem.messageIndex)}
567
+ />
568
+ <Button
569
+ type="text"
570
+ size="small"
571
+ icon={<CopyOutlined />}
572
+ onClick={() => copyMessageContent(messageItem.content || '')}
573
+ />
574
+ <Button
575
+ type="text"
576
+ size="small"
577
+ icon={<MenuUnfoldOutlined />}
578
+ onClick={() => openDrawer('Team workspace')}
579
+ />
580
+ <Button
581
+ type="text"
582
+ size="small"
583
+ icon={<BoxPlotOutlined />}
584
+ onClick={() => openDrawer('trace')}
585
+ />
586
+ <Button
587
+ type="text"
588
+ size="small"
589
+ icon={<AlertFilled />}
590
+ onClick={() => window.open('/trace_ui.html', '_blank')}
591
+ />
592
+ </div>
593
+ ),
594
+ loadingRender: () => <Spin size="small" />,
595
+ },
596
+ user: { placement: 'end' },
597
+ }}
598
+ />
599
+ ) : (
600
+ <div
601
+ className={styles.placeholder}
602
+ >
603
+ <Welcome
604
+ onSubmit={(v: string) => {
605
+ if (v && v.trim()) {
606
+ onSubmit(v);
607
+ setInputValue('');
608
+ }
609
+ }}
610
+ models={models}
611
+ selectedModel={selectedModel}
612
+ onModelChange={handleModelChange}
613
+ modelsLoading={modelsLoading}
614
+ />
615
+ </div>
616
+ )}
617
+ </div>
618
+ );
619
+ const senderHeader = (
620
+ <Sender.Header
621
+ title="Upload File"
622
+ open={attachmentsOpen}
623
+ onOpenChange={setAttachmentsOpen}
624
+ styles={{ content: { padding: 0 } }}
625
+ >
626
+ <Attachments
627
+ beforeUpload={() => false}
628
+ items={attachedFiles}
629
+ onChange={(info) => setAttachedFiles(info.fileList)}
630
+ placeholder={(type) =>
631
+ type === 'drop'
632
+ ? { title: 'Drop file here' }
633
+ : {
634
+ icon: <CloudUploadOutlined />,
635
+ title: 'Upload files',
636
+ description: 'Click or drag files to this area to upload',
637
+ }
638
+ }
639
+ />
640
+ </Sender.Header>
641
+ );
642
+ const chatSender = (
643
+ <>
644
+ {/* 🌟 提示词 */}
645
+ <Prompts
646
+ items={SENDER_PROMPTS}
647
+ onItemClick={(info) => {
648
+ const description = info.data.description as string;
649
+ if (description && description.trim()) {
650
+ onSubmit(description);
651
+ }
652
+ }}
653
+ className={styles.senderPrompt}
654
+ />
655
+ {/* 🌟 输入框 */}
656
+ <Sender
657
+ value={inputValue}
658
+ header={senderHeader}
659
+ onSubmit={() => {
660
+ if (inputValue.trim()) {
661
+ onSubmit(inputValue);
662
+ setInputValue('');
663
+ }
664
+ }}
665
+ onChange={setInputValue}
666
+ onCancel={() => {
667
+ abortController.current?.abort();
668
+ }}
669
+ prefix={
670
+ <Button
671
+ type="text"
672
+ icon={<PaperClipOutlined style={{ fontSize: 18 }} />}
673
+ onClick={() => setAttachmentsOpen(!attachmentsOpen)}
674
+ />
675
+ }
676
+ loading={loading}
677
+ className={styles.sender}
678
+ allowSpeech
679
+ actions={(_, info) => {
680
+ const { SendButton, LoadingButton, SpeechButton } = info.components;
681
+ return (
682
+ <Flex gap={4}>
683
+ <SpeechButton className={styles.speechButton} />
684
+ {loading ? (
685
+ <LoadingButton type="default" />
686
+ ) : (
687
+ <SendButton
688
+ type="primary"
689
+ disabled={!inputValue.trim()}
690
+ className={styles.sendButton}
691
+ />
692
+ )}
693
+ </Flex>
694
+ );
695
+ }}
696
+ placeholder="Ask or input / use skills"
697
+ />
698
+ </>
699
+ );
700
+
701
+ useEffect(() => {
702
+ if (messages?.length && curConversation) {
703
+ setMessageHistory((prev) => ({
704
+ ...prev,
705
+ [curConversation]: messages,
706
+ }));
707
+ }
708
+ }, [messages, curConversation]);
709
+
710
+ // ==================== Render =================
711
+ return (
712
+ <div className={styles.layout}>
713
+ {chatSider}
714
+ <div className={styles.chat}>
715
+ {chatList}
716
+ {(messages?.length > 0 || curConversation) && chatSender}
717
+ </div>
718
+ <Drawer
719
+ placement="right"
720
+ width={700}
721
+ title={drawerContent}
722
+ extra={
723
+ <ShrinkOutlined
724
+ onClick={closeDrawer}
725
+ style={{
726
+ fontSize: '18px',
727
+ color: '#444',
728
+ cursor: 'pointer'
729
+ }}
730
+ />
731
+ }
732
+ onClose={closeDrawer}
733
+ closable={false}
734
+ maskClosable={true}
735
+ open={drawerVisible}
736
+ >
737
+ {drawerContent === 'Team workspace' ? <Workspace sessionId={sessionId} /> : <Trace sessionId={sessionId} />}
738
+ </Drawer>
739
+ </div>
740
+ );
741
+ };
742
+
743
+ export default App;
aworld/cmd/web/webui/src/pages/app.less ADDED
@@ -0,0 +1 @@
 
 
1
+
aworld/cmd/web/webui/src/pages/index.less ADDED
File without changes