feat(web): 用markdown显示思考过程

This commit is contained in:
v-zhangjc9
2025-06-03 16:12:23 +08:00
parent 602a337923
commit c9a1ea2be5
4 changed files with 106 additions and 22 deletions

View File

@@ -15,6 +15,7 @@
"@echofly/fetch-event-source": "^3.0.2",
"@fortawesome/fontawesome-free": "^6.7.2",
"@tinyflow-ai/react": "^0.1.10",
"ahooks": "^3.8.5",
"amis": "^6.12.0",
"antd": "^5.25.3",
"axios": "^1.9.0",

View File

@@ -26,6 +26,9 @@ importers:
'@tinyflow-ai/react':
specifier: ^0.1.10
version: 0.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(svelte@5.28.2)
ahooks:
specifier: ^3.8.5
version: 3.8.5(react@18.3.1)
amis:
specifier: ^6.12.0
version: 6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-ui@6.12.0(@types/react@18.3.23)(amis-core@6.12.0(@types/react@18.3.23)(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react-is@18.3.1)(react@18.3.1))(amis-formula@6.12.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(office-viewer@0.3.14(echarts@5.5.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -1077,6 +1080,12 @@ packages:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
ahooks@3.8.5:
resolution: {integrity: sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw==}
engines: {node: '>=8.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
@@ -1863,6 +1872,9 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
intersection-observer@0.12.2:
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@@ -1898,6 +1910,10 @@ packages:
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-cookie@3.0.5:
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
engines: {node: '>=14'}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -2659,6 +2675,9 @@ packages:
peerDependencies:
react: '>= 16.8'
react-fast-compare@3.2.2:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
react-hook-form@7.39.0:
resolution: {integrity: sha512-rekW5NMBVG0nslE2choOKThy0zxLWQeoew87yTLwb3C9F91LaXwu/dhfFL/D3hdnSMnrTG60gVN/v6rvCrSOTw==}
engines: {node: '>=12.22.0'}
@@ -2825,6 +2844,10 @@ packages:
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
screenfull@5.2.0:
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
engines: {node: '>=0.10.0'}
scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
@@ -4262,6 +4285,19 @@ snapshots:
- supports-color
optional: true
ahooks@3.8.5(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.3
dayjs: 1.11.13
intersection-observer: 0.12.2
js-cookie: 3.0.5
lodash: 4.17.21
react: 18.3.1
react-fast-compare: 3.2.2
resize-observer-polyfill: 1.5.1
screenfull: 5.2.0
tslib: 2.8.1
ajv@6.12.6:
dependencies:
fast-deep-equal: 3.1.3
@@ -5299,6 +5335,8 @@ snapshots:
inherits@2.0.4: {}
intersection-observer@0.12.2: {}
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
@@ -5326,6 +5364,8 @@ snapshots:
isexe@2.0.0: {}
js-cookie@3.0.5: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
@@ -5828,7 +5868,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.27.3
'@rc-component/mini-decimal': 1.1.0
classnames: 2.3.2
classnames: 2.5.1
rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -5925,7 +5965,7 @@ snapshots:
rc-progress@3.4.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.27.3
classnames: 2.3.2
classnames: 2.5.1
rc-util: 5.44.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
@@ -6146,6 +6186,8 @@ snapshots:
prop-types: 15.8.1
react: 18.3.1
react-fast-compare@3.2.2: {}
react-hook-form@7.39.0(react@18.3.1):
dependencies:
react: 18.3.1
@@ -6362,6 +6404,8 @@ snapshots:
dependencies:
loose-envify: 1.4.0
screenfull@5.2.0: {}
scroll-into-view-if-needed@3.1.0:
dependencies:
compute-scroll-into-view: 3.1.1

View File

@@ -1,7 +1,9 @@
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, Divider, Flex, Popover, Radio, Switch, Tooltip, Typography} from 'antd'
import {isEqual, isStrBlank, trim} from 'licia'
import markdownIt from 'markdown-it'
import {useRef, useState} from 'react'
import styled from 'styled-components'
@@ -32,7 +34,6 @@ const ConversationDiv = styled.div`
border-left: 3px solid;
padding-left: 5px;
margin-bottom: 10px;
white-space: pre-line;
}
}
@@ -43,23 +44,35 @@ const ConversationDiv = styled.div`
}
`
type ChatMessage = { role: string, content?: string, reason?: string }
function Conversation() {
const abortController = useRef<AbortController | null>(null)
const [input, setInput] = useState<string>('')
const [think, setThink] = useState<boolean>(true)
const [knowledge, setKnowledge] = useState<string>('0')
const [knowledgeList, setKnowledgeList] = useState<{ id: string, name: string }[]>([])
const [agent] = useXAgent<{ role: string, content: 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) => {
await fetchEventSource(`${commonInfo.baseAiChatUrl}/chat/async`, {
let requestUrl = `${commonInfo.baseAiChatUrl}/chat/async`
if (!isEqual('0', knowledge)) {
requestUrl = `${requestUrl}?knowledge=${knowledge}`
}
await fetchEventSource(requestUrl, {
method: 'POST',
headers: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
'Content-Type': 'application/json',
},
headers: commonInfo.authorizationHeaders,
body: JSON.stringify(info.messages),
signal: abortController.current?.signal,
onmessage: ev => {
console.log(ev)
callbacks.onUpdate({
id: ev.id,
event: 'delta',
@@ -73,17 +86,22 @@ function Conversation() {
const {onRequest, messages, setMessages} = useXChat({
agent,
transformMessage: ({originMessage, chunk}) => {
let text = ''
let content = '', reason = ''
try {
if (chunk?.data) {
text = 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 {
content: (originMessage?.content || '') + text,
role: 'assistant',
content: (originMessage?.content || '') + content,
reason: (originMessage?.reason || '') + reason,
}
},
resolveAbortController: controller => {
@@ -105,10 +123,17 @@ function Conversation() {
background: 'transparent',
},
},
messageRender: content => {
messageRender: item => {
let content = ''
if (!isStrBlank(item['reason'])) {
content = `<think>${trim(md.render(item['reason']))}</think>${trim(md.render(item['content']))}`
} else {
content = trim(md.render(item['content']))
}
console.log(content)
return (
<Typography>
<div dangerouslySetInnerHTML={{__html: md.render(content)}}/>
<div dangerouslySetInnerHTML={{__html: content}}/>
</Typography>
)
},
@@ -118,12 +143,20 @@ function Conversation() {
avatar: {
icon: <UserOutlined/>,
},
messageRender: item => {
return (
<Typography>{trim(item['content'])}</Typography>
)
},
},
}}
items={messages.map(({id, message}) => ({
key: id,
...message,
}))}
items={messages.map(({id, message}) => {
return {
key: id,
role: message.role,
content: message,
}
})}
/>)
: (<div className="conversation-welcome">
<Welcome
@@ -170,10 +203,12 @@ function Conversation() {
flexDirection: 'column',
gap: 10,
}}
disabled={agent.isRequesting()}
value={knowledge}
onChange={event => setKnowledge(event.target.value)}
options={[
{value: 1, label: '测试'},
{value: 2, label: 'Hudi'},
{value: 3, label: 'Apache Hudi'},
{value: '0', label: ''},
...knowledgeList.map(k => ({label: k.name, value: k.id})),
]}
/>}
>

View File

@@ -13,6 +13,10 @@ export const commonInfo = {
baseAiChatUrl: 'http://132.126.207.130:35690/hudi_services/ai_chat',
baseAiKnowledgeUrl: 'http://132.126.207.130:35690/hudi_services/ai_knowledge',
// baseUrl: '/hudi_services/service_web',
authorizationHeaders: {
'Authorization': 'Basic QXhoRWJzY3dzSkRiWU1IMjpjWXhnM2I0UHRXb1ZENVNqRmF5V3h0blNWc2p6UnNnNA==',
'Content-Type': 'application/json',
},
clusters: {
// hudi同步运行集群和yarn队列名称
sync: {