250 lines
7.9 KiB
TypeScript
250 lines
7.9 KiB
TypeScript
import {ClearOutlined, FileOutlined, UserOutlined} from '@ant-design/icons'
|
|
import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x'
|
|
import {fetchEventSource} from '@echofly/fetch-event-source'
|
|
import {useMount} from 'ahooks'
|
|
import {Button, Collapse, Flex, Popover, Radio, Typography} from 'antd'
|
|
import {isEqual, isStrBlank, trim} from 'licia'
|
|
import markdownIt from 'markdown-it'
|
|
import {useRef, useState} from 'react'
|
|
import styled from 'styled-components'
|
|
import {commonInfo} from '../../util/amis.tsx'
|
|
|
|
const md = markdownIt({html: true, breaks: true})
|
|
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>('')
|
|
const [knowledge, setKnowledge] = useState<string>('0')
|
|
const [knowledgeList, setKnowledgeList] = useState<{ id: string, name: string }[]>([])
|
|
|
|
useMount(async () => {
|
|
let response = await fetch(`${commonInfo.baseAiKnowledgeUrl}/knowledge/list`, {
|
|
headers: commonInfo.authorizationHeaders,
|
|
})
|
|
let items = (await response.json()).data.items
|
|
setKnowledgeList(items.map((item: { id: string, name: string }) => ({id: item.id, name: item.name})))
|
|
})
|
|
|
|
const [agent] = useXAgent<ChatMessage>({
|
|
request: async (info, callbacks) => {
|
|
let requestUrl = `${commonInfo.baseAiChatUrl}/chat/async`
|
|
if (!isEqual('0', info.knowledge)) {
|
|
requestUrl = `${requestUrl}?knowledge_id=${info.knowledge}`
|
|
}
|
|
await fetchEventSource(requestUrl, {
|
|
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([]),
|
|
})
|
|
},
|
|
})
|
|
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(md.render(item['reason']))}`
|
|
}
|
|
content = trim(md.render(item['content']))
|
|
return (
|
|
<div>
|
|
{isStrBlank(reason)
|
|
? <span/>
|
|
: <Collapse
|
|
size="small"
|
|
defaultActiveKey={0}
|
|
items={[
|
|
{
|
|
key: 0,
|
|
label: '思考链',
|
|
children: (
|
|
<Typography>
|
|
<div dangerouslySetInnerHTML={{__html: reason}}/>
|
|
</Typography>
|
|
),
|
|
},
|
|
]}
|
|
/>}
|
|
<Typography>
|
|
<div dangerouslySetInnerHTML={{__html: content}}/>
|
|
</Typography>
|
|
</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,
|
|
knowledge: knowledge,
|
|
})
|
|
setInput('')
|
|
}}
|
|
onCancel={() => abortController.current?.abort()}
|
|
footer={({components}) => {
|
|
const {SendButton, LoadingButton} = components
|
|
return (
|
|
<Flex justify="space-between" align="center">
|
|
<Flex gap="small" align="center">
|
|
<Popover
|
|
title="选择知识库"
|
|
trigger="hover"
|
|
content={<Radio.Group
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 10,
|
|
}}
|
|
disabled={agent.isRequesting()}
|
|
value={knowledge}
|
|
onChange={event => setKnowledge(event.target.value)}
|
|
options={[
|
|
{value: '0', label: '无'},
|
|
...knowledgeList.map(k => ({label: k.name, value: k.id})),
|
|
]}
|
|
/>}
|
|
>
|
|
<Button
|
|
icon={<FileOutlined/>}
|
|
type="text"
|
|
size="small"
|
|
>
|
|
知识库
|
|
</Button>
|
|
</Popover>
|
|
<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 |