feat(web): 增加AI对话的能力

This commit is contained in:
v-zhangjc9
2025-05-13 16:03:08 +08:00
parent 819d56fbe3
commit dd2e56e27b
4 changed files with 247 additions and 9 deletions

View File

@@ -18,12 +18,14 @@
"antd": "^5.25.0",
"axios": "^1.9.0",
"licia": "^1.48.0",
"markdown-it": "^14.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router": "^7.5.3",
"styled-components": "^6.1.18"
},
"devDependencies": {
"@types/markdown-it": "^14.1.2",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react-swc": "^3.9.0",

View File

@@ -35,6 +35,9 @@ importers:
licia:
specifier: ^1.48.0
version: 1.48.0
markdown-it:
specifier: ^14.1.0
version: 14.1.0
react:
specifier: ^18.2.0
version: 18.3.1
@@ -48,6 +51,9 @@ importers:
specifier: ^6.1.18
version: 6.1.18(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@types/markdown-it':
specifier: ^14.1.2
version: 14.1.2
'@types/react':
specifier: ^18.2.0
version: 18.3.21
@@ -926,6 +932,15 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/linkify-it@5.0.0':
resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==}
'@types/markdown-it@14.1.2':
resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==}
'@types/mdurl@2.0.0':
resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==}
'@types/node@14.18.63':
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
@@ -1474,6 +1489,10 @@ packages:
entities@2.1.0:
resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
@@ -1903,6 +1922,9 @@ packages:
linkify-it@3.0.3:
resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==}
linkify-it@5.0.0:
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
listenercount@1.0.1:
resolution: {integrity: sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==}
@@ -1996,6 +2018,10 @@ packages:
resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==}
hasBin: true
markdown-it@14.1.0:
resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
hasBin: true
match-sorter@6.3.4:
resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==}
@@ -2009,6 +2035,9 @@ packages:
mdurl@1.0.1:
resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==}
mdurl@2.0.0:
resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
@@ -2292,6 +2321,10 @@ packages:
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
punycode.js@2.3.1:
resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
engines: {node: '>=6'}
punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -2992,6 +3025,9 @@ packages:
uc.micro@1.0.6:
resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==}
uc.micro@2.1.0:
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
uncontrollable@7.2.1:
resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==}
peerDependencies:
@@ -4024,6 +4060,15 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/linkify-it@5.0.0': {}
'@types/markdown-it@14.1.2':
dependencies:
'@types/linkify-it': 5.0.0
'@types/mdurl': 2.0.0
'@types/mdurl@2.0.0': {}
'@types/node@14.18.63': {}
'@types/prop-types@15.7.14': {}
@@ -4771,6 +4816,8 @@ snapshots:
entities@2.1.0: {}
entities@4.5.0: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
@@ -5285,6 +5332,10 @@ snapshots:
dependencies:
uc.micro: 1.0.6
linkify-it@5.0.0:
dependencies:
uc.micro: 2.1.0
listenercount@1.0.1: {}
locate-character@3.0.0: {}
@@ -5360,6 +5411,15 @@ snapshots:
mdurl: 1.0.1
uc.micro: 1.0.6
markdown-it@14.1.0:
dependencies:
argparse: 2.0.1
entities: 4.5.0
linkify-it: 5.0.0
mdurl: 2.0.0
punycode.js: 2.3.1
uc.micro: 2.1.0
match-sorter@6.3.4:
dependencies:
'@babel/runtime': 7.27.1
@@ -5371,6 +5431,8 @@ snapshots:
mdurl@1.0.1: {}
mdurl@2.0.0: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
@@ -5616,6 +5678,8 @@ snapshots:
proxy-from-env@1.1.0: {}
punycode.js@2.3.1: {}
punycode@2.3.1: {}
pure-color@1.3.0: {}
@@ -6511,6 +6575,8 @@ snapshots:
uc.micro@1.0.6: {}
uc.micro@2.1.0: {}
uncontrollable@7.2.1(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.1

View File

@@ -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>
)
}

View File

@@ -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/>,
},
],
},
],