Files
hudi-service/service-web/client/src/pages/ai/Conversation.tsx

210 lines
6.2 KiB
TypeScript

import {ClearOutlined, UserOutlined} from '@ant-design/icons'
import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x'
import {fetchEventSource} from '@echofly/fetch-event-source'
import {useUnmount} from 'ahooks'
import {Button, Collapse, Flex, Typography} from 'antd'
import {isStrBlank, trim} from 'licia'
import {useRef, useState} from 'react'
import styled from 'styled-components'
import {commonInfo} from '../../util/amis.tsx'
import MarkdownRender from '../../util/Markdown.tsx'
const ConversationDiv = styled.div`
height: calc(100vh - 76px);
display: flex;
flex-direction: column;
padding: 10px;
.conversation-welcome {
flex: 1;
width: 70%;
margin: 30px auto 30px;
}
.conversation-list {
flex: 1;
margin-bottom: 30px;
padding-left: 30px;
padding-right: 30px;
}
.conversation-sender {
height: 100px;
padding-left: 30px;
padding-right: 30px;
}
`
type ChatMessage = { role: string, content?: string, reason?: string }
function Conversation() {
const abortController = useRef<AbortController | null>(null)
const [input, setInput] = useState<string>('')
useUnmount(() => {
console.log('Page Unmount')
abortController.current?.abort()
})
const [agent] = useXAgent<ChatMessage>({
request: async (info, callbacks) => {
await fetchEventSource(`${commonInfo.baseAiUrl}/chat/async`, {
method: 'POST',
headers: commonInfo.authorizationHeaders,
body: JSON.stringify(info.messages),
signal: abortController.current?.signal,
onmessage: ev => {
callbacks.onUpdate({
id: ev.id,
event: 'delta',
data: ev.data,
})
},
onclose: () => callbacks.onSuccess([]),
onerror: error => callbacks.onError(error),
})
},
})
const {onRequest, messages, setMessages} = useXChat({
agent,
transformMessage: ({originMessage, chunk}) => {
let content = '', reason = ''
try {
if (chunk?.data) {
let map = JSON.parse(chunk.data)
if (map['content'])
content = map['content']
if (map['reason'])
reason = map['reason']
}
} catch (error) {
console.error(error)
}
return {
role: 'assistant',
content: (originMessage?.content || '') + content,
reason: (originMessage?.reason || '') + reason,
}
},
resolveAbortController: controller => {
abortController.current = controller
},
})
return (
<ConversationDiv>
{messages.length > 0
? (<Bubble.List
className="conversation-list"
roles={{
assistant: {
placement: 'start',
avatar: {
icon: <img src="icon.png" alt=""/>,
style: {
background: 'transparent',
},
},
messageRender: item => {
let content = '', reason = ''
if (!isStrBlank(item['reason'])) {
reason = trim(item['reason'])
}
content = trim(item['content'])
return (
<div>
{isStrBlank(reason)
? <span/>
: <Collapse
size="small"
defaultActiveKey={0}
items={[
{
key: 0,
label: '思考链',
children: (
<MarkdownRender content={reason}/>
),
},
]}
/>}
<MarkdownRender content={content}/>
</div>
)
},
},
user: {
placement: 'end',
avatar: {
icon: <UserOutlined/>,
},
messageRender: item => {
return (
<Typography>{trim(item['content'])}</Typography>
)
},
},
}}
items={messages.map(({id, message}) => {
return {
key: id,
role: message.role,
content: message,
}
})}
/>)
: (<div className="conversation-welcome">
<Welcome
variant="borderless"
icon={<img src="icon.png" alt="icon"/>}
title="你好,我是基于大模型深度思考技术构建的 AI 运营助手"
description="我可以帮你查询、检索Hudi 服务的运行情况,分析、处理 Hudi 服务的运营故障,输出、解读 Hudi 系统整体运营报告"
/>
</div>)}
<div className="conversation-sender">
<Sender
value={input}
onChange={setInput}
onSubmit={message => {
onRequest({
message: {
role: 'user',
content: message,
},
stream: true,
})
setInput('')
}}
onCancel={() => abortController.current?.abort()}
footer={({components}) => {
const {SendButton, LoadingButton} = components
return (
<Flex justify="space-between" align="center">
<Flex gap="small" align="center">
<Button
icon={<ClearOutlined/>}
type="text"
size="small"
onClick={() => setMessages([])}
>
</Button>
</Flex>
<Flex align="center">
{agent.isRequesting() ? (
<LoadingButton type="default"/>
) : (
<SendButton type="primary" disabled={false}/>
)}
</Flex>
</Flex>
)
}}
actions={false}
/>
</div>
</ConversationDiv>
)
}
export default Conversation