From c9a1ea2be5694d0d08a4c0d590d3f18d3b8f29e1 Mon Sep 17 00:00:00 2001 From: v-zhangjc9 Date: Tue, 3 Jun 2025 16:12:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E7=94=A8markdown=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=80=9D=E8=80=83=E8=BF=87=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- service-web/client/package.json | 1 + service-web/client/pnpm-lock.yaml | 48 +++++++++++- .../client/src/pages/ai/Conversation.tsx | 75 ++++++++++++++----- service-web/client/src/util/amis.tsx | 4 + 4 files changed, 106 insertions(+), 22 deletions(-) diff --git a/service-web/client/package.json b/service-web/client/package.json index 2a7b21d..d823897 100644 --- a/service-web/client/package.json +++ b/service-web/client/package.json @@ -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", diff --git a/service-web/client/pnpm-lock.yaml b/service-web/client/pnpm-lock.yaml index c9b623e..9e9e15e 100644 --- a/service-web/client/pnpm-lock.yaml +++ b/service-web/client/pnpm-lock.yaml @@ -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 diff --git a/service-web/client/src/pages/ai/Conversation.tsx b/service-web/client/src/pages/ai/Conversation.tsx index ff1d186..248f9e1 100644 --- a/service-web/client/src/pages/ai/Conversation.tsx +++ b/service-web/client/src/pages/ai/Conversation.tsx @@ -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(null) const [input, setInput] = useState('') const [think, setThink] = useState(true) + const [knowledge, setKnowledge] = useState('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({ 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 = `${trim(md.render(item['reason']))}${trim(md.render(item['content']))}` + } else { + content = trim(md.render(item['content'])) + } + console.log(content) return ( -
+
) }, @@ -118,12 +143,20 @@ function Conversation() { avatar: { icon: , }, + messageRender: item => { + return ( + {trim(item['content'])} + ) + }, }, }} - items={messages.map(({id, message}) => ({ - key: id, - ...message, - }))} + items={messages.map(({id, message}) => { + return { + key: id, + role: message.role, + content: message, + } + })} />) : (
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})), ]} />} > diff --git a/service-web/client/src/util/amis.tsx b/service-web/client/src/util/amis.tsx index 881f53f..ef7ba10 100644 --- a/service-web/client/src/util/amis.tsx +++ b/service-web/client/src/util/amis.tsx @@ -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: {