feat(web): 增加AI对话的能力
This commit is contained in:
@@ -1,8 +1,178 @@
|
||||
import {ClearOutlined, UserOutlined} from '@ant-design/icons'
|
||||
import {Bubble, Sender, useXAgent, useXChat, Welcome} from '@ant-design/x'
|
||||
import {Button, Divider, Flex, Switch, Tooltip, Typography} from 'antd'
|
||||
import markdownIt from 'markdown-it'
|
||||
import {useRef, useState} from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
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;
|
||||
|
||||
think {
|
||||
color: gray;
|
||||
display: block;
|
||||
border-left: 3px solid;
|
||||
padding-left: 5px;
|
||||
margin-bottom: 10px;
|
||||
//white-space: pre-line;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-sender {
|
||||
height: 100px;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
`
|
||||
const llmConfig = {
|
||||
base: 'http://132.121.206.65:10086',
|
||||
model: 'Qwen3-1.7',
|
||||
secret: 'Bearer *XMySqV%>hR&v>>g*NwCs3tpQ5FVMFEF2VHVTj<MYQd$&@$sY7CgqNyea4giJi4',
|
||||
}
|
||||
|
||||
function Conversation() {
|
||||
const abortController = useRef<AbortController | null>(null)
|
||||
const [input, setInput] = useState<string>('')
|
||||
const [think, setThink] = useState<boolean>(true)
|
||||
|
||||
const [agent] = useXAgent<{ role: string, content: string }>({
|
||||
baseURL: `${llmConfig.base}/v1/chat/completions`,
|
||||
model: llmConfig.model,
|
||||
dangerouslyApiKey: llmConfig.secret,
|
||||
})
|
||||
const {onRequest, messages, setMessages} = useXChat({
|
||||
agent,
|
||||
transformMessage: ({originMessage, chunk}) => {
|
||||
let text = ''
|
||||
try {
|
||||
if (chunk?.data && !chunk?.data.includes('DONE')) {
|
||||
const message = JSON.parse(chunk?.data)
|
||||
text = !message?.choices?.[0].delta?.content
|
||||
? ''
|
||||
: message?.choices?.[0].delta?.content
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
return {
|
||||
content: (originMessage?.content || '') + text,
|
||||
role: 'assistant',
|
||||
}
|
||||
},
|
||||
resolveAbortController: controller => {
|
||||
abortController.current = controller
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="conversation">
|
||||
Conversation
|
||||
</div>
|
||||
<ConversationDiv>
|
||||
{messages.length > 0
|
||||
? (<Bubble.List
|
||||
className="conversation-list"
|
||||
roles={{
|
||||
assistant: {
|
||||
placement: 'start',
|
||||
avatar: {
|
||||
icon: <img src="icon.png" alt=""/>,
|
||||
style: {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
messageRender: content => {
|
||||
let split = content.split('</think>')
|
||||
if (split.length > 1) {
|
||||
content = `${split[0]}</think>${md.render(split[1])}`
|
||||
}
|
||||
return (
|
||||
<Typography>
|
||||
<div dangerouslySetInnerHTML={{__html: content}}/>
|
||||
</Typography>
|
||||
)
|
||||
},
|
||||
},
|
||||
user: {
|
||||
placement: 'end',
|
||||
avatar: {
|
||||
icon: <UserOutlined/>,
|
||||
},
|
||||
},
|
||||
}}
|
||||
items={messages.map(({id, message}) => ({
|
||||
key: id,
|
||||
...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
|
||||
loading={agent.isRequesting()}
|
||||
value={input}
|
||||
onChange={setInput}
|
||||
onSubmit={message => {
|
||||
onRequest({
|
||||
message: {
|
||||
role: 'user',
|
||||
content: think ? message : `/no_think ${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">
|
||||
深度思考
|
||||
<Switch size="small" value={think} onChange={setThink}/>
|
||||
<Divider type="vertical"/>
|
||||
<Tooltip title="清空对话">
|
||||
<Button
|
||||
icon={<ClearOutlined/>}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={() => setMessages([])}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Flex align="center">
|
||||
{agent.isRequesting() ? (
|
||||
<LoadingButton type="default"/>
|
||||
) : (
|
||||
<SendButton type="primary" disabled={false}/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
)
|
||||
}}
|
||||
actions={false}
|
||||
/>
|
||||
</div>
|
||||
</ConversationDiv>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export const routes: RouteObject[] = [
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="/ai/inspection" replace/>,
|
||||
element: <Navigate to="/ai/conversation" replace/>,
|
||||
},
|
||||
{
|
||||
path: 'inspection',
|
||||
@@ -169,16 +169,16 @@ export const menus = {
|
||||
name: 'AI',
|
||||
icon: <OpenAIOutlined/>,
|
||||
routes: [
|
||||
{
|
||||
path: '/ai/inspection',
|
||||
name: '智能巡检',
|
||||
icon: <CheckSquareOutlined/>,
|
||||
},
|
||||
{
|
||||
path: '/ai/conversation',
|
||||
name: '智慧问答',
|
||||
icon: <QuestionOutlined/>,
|
||||
},
|
||||
{
|
||||
path: '/ai/inspection',
|
||||
name: '智能巡检',
|
||||
icon: <CheckSquareOutlined/>,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user