mirror of
http://112.124.100.131/ebiz-ai/ebiz-base-ai.git
synced 2025-12-06 09:26:48 +08:00
feat(AI): 优化聊天组件并添加新功能
- 移除 request 和 response 拦截器中的 console.log 语句- 优化消息组件样式和逻辑,支持渐进式消息显示- 更新 TabBox 组件,支持动态列表数据 - 重构 treasureBox 组件,展示产品详情和知识库 - 优化 AI聊天流程,支持流式响应和消息动画 - 添加 markdown 渲染支持,包括数学公式和流程图
This commit is contained in:
@@ -24,8 +24,10 @@
|
||||
"eruda": "^2.11.3",
|
||||
"fastclick": "^1.0.6",
|
||||
"markdown-it": "^12.3.2",
|
||||
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"markdown-it-mermaid": "^0.2.5",
|
||||
"mermaid": "^10.9.3",
|
||||
"mermaid-it-markdown": "^1.0.8",
|
||||
"sass": "^1.69.3",
|
||||
"svg-sprite-loader": "^6.0.11",
|
||||
"swiper": "^5.4.5",
|
||||
@@ -45,13 +47,18 @@
|
||||
"@vue/cli-service": "^3.9.0",
|
||||
"@vue/eslint-config-prettier": "^4.0.1",
|
||||
"@vue/test-utils": "1.0.0-beta.29",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-core": "7.0.0-bridge.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-plugin-import": "^1.12.0",
|
||||
"browserslist": "^4.25.0",
|
||||
"caniuse-lite": "^1.0.30001721",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-plugin-prettier": "^3.1.0",
|
||||
"eslint-plugin-vue": "^5.0.0",
|
||||
"postcss": "^8.5.4",
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-px-to-viewport": "^1.1.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vee-validate": "^2.0.0-rc.25",
|
||||
|
||||
@@ -8,7 +8,6 @@ const service = axios.create({
|
||||
// request拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
console.log(config)
|
||||
if (!config.headers.noLoading) {
|
||||
Toast.loading({
|
||||
duration: 0,
|
||||
@@ -30,7 +29,6 @@ service.interceptors.request.use(
|
||||
// respone拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
console.log(response, 23)
|
||||
if (response.config.sts) {
|
||||
return res
|
||||
}
|
||||
@@ -85,7 +83,6 @@ service.interceptors.response.use(
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
console.log(error)
|
||||
Toast(error.message ? error.message : '未知异常')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div >
|
||||
<div class='tab-box' v-for='item in 2'>
|
||||
<div class='tab-box' v-for='item in list'>
|
||||
<div class='box-title'>
|
||||
这个标题
|
||||
{{item.title}}
|
||||
</div>
|
||||
<div class='box-container'>
|
||||
<div v-for='item in 5'>
|
||||
内容
|
||||
<div v-for='list in item.list'>
|
||||
{{list.title}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,7 +18,12 @@ export default {
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
props: {},
|
||||
props: {
|
||||
list: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
watch: {},
|
||||
components: {},
|
||||
filters: {},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<section>
|
||||
<div v-for="(message, index) in messagesList" :key="index" class="message-item">
|
||||
<div v-for="(message, index) in messagesList" :key="index" class="message-item ">
|
||||
<!--用户消息-->
|
||||
<div v-if="message.type === 'user'" class="user-message">
|
||||
<div v-if="message.type === 'user'" class="user-message mb10">
|
||||
<p>{{ message.text }}</p>
|
||||
</div>
|
||||
<!--机器人消息-->
|
||||
<div v-else-if="message.type === 'bot'" class="bot-message">
|
||||
<div v-else-if="message.type === 'bot'" class="bot-message mb10">
|
||||
<span v-if="message.isThink">
|
||||
<!-- loading-->
|
||||
<span class="speakLoadingToast pv10">
|
||||
@@ -18,18 +18,18 @@
|
||||
<p v-html="md.render(message.think)" v-if="message.think && message.showThink" class="thinkText" />
|
||||
</span>
|
||||
<div>
|
||||
<p v-html="md.render(message.text)" v-if="message.text"></p>
|
||||
<span class="speakLoadingToast pv10" v-else>
|
||||
<p v-html="md.render(message.text)" v-if="message.text "></p>
|
||||
<span class="speakLoadingToast pv10" v-else-if='!message.text && !thinkOk'>
|
||||
<van-loading type="spinner" color="#2e5ca9" size="20px" v-if="!message.text" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--百宝箱-->
|
||||
<div v-else>
|
||||
<div v-else class='mb10'>
|
||||
<treasure-box :item="message"></treasure-box>
|
||||
</div>
|
||||
<!-- 新增点赞和踩按钮 -->
|
||||
<div class="reaction-buttons" v-if="message.type !== 'user'">
|
||||
<div class="reaction-buttons mb10" v-if="message.type !== 'user'">
|
||||
<button @click="handleReaction(message, 'like')" class="like">
|
||||
<svg-icon :icon-class="message.isLike ? 'fillLike' : 'like'" class-name="chat-icon"></svg-icon> 有用
|
||||
</button>
|
||||
@@ -47,16 +47,12 @@ import { Icon } from 'vant'
|
||||
import TreasureBox from '@/views/AI/components/treasureBox.vue'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
import markdownItKatex from 'markdown-it-katex'
|
||||
// import mermaidItMarkdown from 'mermaid-it-markdown'
|
||||
|
||||
console.log(mermaidItMarkdown)
|
||||
const md = new MarkdownIt({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
})
|
||||
.use(markdownItKatex)
|
||||
// .use(mermaidItMarkdown)
|
||||
export default {
|
||||
name: 'message',
|
||||
components: { TreasureBox, [Icon.name]: Icon },
|
||||
@@ -65,6 +61,10 @@ export default {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
thinkOk: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isDeep: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -133,6 +133,8 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); // 使用rgba定义颜色,透
|
||||
|
||||
.user-message {
|
||||
justify-content: end;
|
||||
max-width: 80%;
|
||||
margin-left: 20%;
|
||||
}
|
||||
.bot-message {
|
||||
justify-content: start;
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
<div class="treasure-box">
|
||||
<h3>{{ item.text.indexOf('工具箱') !== -1 ? item.text : item.text + '工具箱' }}</h3>
|
||||
<van-tabs v-model="active" color="#2E5CA9" title-active-color="#2E5CA9" line-width="20">
|
||||
<van-tab title="常用工具">
|
||||
<TabBox></TabBox>
|
||||
</van-tab>
|
||||
<van-tab title="爆款图文">
|
||||
<TabBox></TabBox>
|
||||
</van-tab>
|
||||
|
||||
<!-- <van-tab title="爆款图文">-->
|
||||
<!-- <TabBox></TabBox>-->
|
||||
<!-- </van-tab>-->
|
||||
<van-tab title="产品知识">
|
||||
<TabBox></TabBox>
|
||||
<TabBox :list='knowledge'></TabBox>
|
||||
</van-tab>
|
||||
<van-tab title="常用工具">
|
||||
<TabBox :list='tools'></TabBox>
|
||||
</van-tab>
|
||||
</van-tabs>
|
||||
<!-- 在这里添加百宝箱的具体内容 -->
|
||||
@@ -37,22 +38,56 @@ export default {
|
||||
return {
|
||||
active: 0,
|
||||
// 可以在这里添加百宝箱相关的数据
|
||||
// 工具列表
|
||||
tools: [],
|
||||
knowledge: [],
|
||||
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'item.text': {
|
||||
handler(newValue, oldValue) {
|
||||
console.log(this.item)
|
||||
if(!this.item.detail){
|
||||
this.getTreasureBox()
|
||||
} else {
|
||||
this.setList(this.item.detail)
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setList(){
|
||||
this.tools = [
|
||||
{title: '工具',
|
||||
list:[
|
||||
{value:this.item.detail.instructionUrl,title:'产品说明书' },
|
||||
{value:this.item.detail.clauseUrl,title:'条款' },
|
||||
]},
|
||||
]
|
||||
|
||||
|
||||
this.knowledge = []
|
||||
|
||||
for(let i in this.item.detail.knowledge){
|
||||
this.knowledge.push({
|
||||
title:i,
|
||||
list:this.item.detail.knowledge[i].split(',').map(item=>{
|
||||
return {
|
||||
title:item,
|
||||
value:item
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
// 可以在这里添加百宝箱相关的功能方法
|
||||
async getTreasureBox() {
|
||||
productDetail({ query: this.item.text }).then((res) => {
|
||||
console.log(res)
|
||||
productDetail({ productName: this.item.text }).then((res) => {
|
||||
if(res){
|
||||
this.$set(this.item,'detail',res.content)
|
||||
this.setList()
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="chat-content">
|
||||
<div class="message-area" ref="messageArea" @scroll="handleScroll">
|
||||
<HotProducts class="mb10" :messagesList.sync="messages"></HotProducts>
|
||||
<messageComponent :messagesList="messages" :is-deep="isDeep" :is-search="isSearching"></messageComponent>
|
||||
<messageComponent :messagesList="messages" :is-deep="isDeep" :is-search="isSearching" :think-ok='isThink'></messageComponent>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -46,6 +46,9 @@ export default {
|
||||
|
||||
data() {
|
||||
return {
|
||||
answerMap:'',
|
||||
timer:null,
|
||||
answerIndex:0,
|
||||
conversationId: '',
|
||||
currentMessage: null,
|
||||
messageStatus: 'stop',
|
||||
@@ -75,13 +78,16 @@ export default {
|
||||
chatProduct({ query: this.newMessage }).then((res) => {
|
||||
if (res) {
|
||||
this.messageStatus = 'stop'
|
||||
this.messages.push({ type: 'box', text: this.newMessage })
|
||||
this.messages.push({ type: 'box', text: this.newMessage,detail:res.content })
|
||||
this.newMessage = ''
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
sendMessage() {
|
||||
if(this.messageStatus === 'send'){
|
||||
return
|
||||
}
|
||||
if (this.newMessage.trim() === '') return
|
||||
|
||||
this.messages.push({ type: 'user', text: this.newMessage })
|
||||
@@ -157,7 +163,10 @@ export default {
|
||||
signal: abortController.signal,
|
||||
body: JSON.stringify(params),
|
||||
})
|
||||
.then((res) => this.processStreamResponse(res))
|
||||
.then(async (res) =>{
|
||||
this.newMessage = ''
|
||||
await this.processStreamResponse(res)
|
||||
})
|
||||
.catch((err) => console.error('请求错误:', err))
|
||||
},
|
||||
|
||||
@@ -198,7 +207,11 @@ export default {
|
||||
const cleanLine = line.replace(/^data:\s*/, '')
|
||||
if (!cleanLine) return null
|
||||
const data = JSON.parse(cleanLine)
|
||||
console.log(data)
|
||||
if(data.answer){
|
||||
this.answerMap+=data.answer
|
||||
}
|
||||
|
||||
|
||||
this.updateConversationState(data)
|
||||
return data
|
||||
} catch (error) {
|
||||
@@ -216,20 +229,34 @@ export default {
|
||||
this.isThink = false
|
||||
}
|
||||
},
|
||||
updateMessageContent({ answer }) {
|
||||
if (!this.currentMessage || !answer) return
|
||||
|
||||
// 使用 requestAnimationFrame 控制文本逐步显示
|
||||
|
||||
if (answer) {
|
||||
if (this.isThink) {
|
||||
this.currentMessage.think += answer
|
||||
} else {
|
||||
this.currentMessage.text += answer
|
||||
}
|
||||
updateMessageContent({ answer, event }) {
|
||||
if(event === 'message_end'){
|
||||
this.messageStatus = 'stop'
|
||||
}
|
||||
this.scrollToBottom()
|
||||
},
|
||||
if (!this.currentMessage || !answer) return;
|
||||
|
||||
const mode = this.isThink ? 'think' : 'text';
|
||||
// 清除之前的动画任务
|
||||
if (this.timer) {
|
||||
cancelAnimationFrame(this.timer);
|
||||
}
|
||||
|
||||
const renderChar = () => {
|
||||
|
||||
|
||||
if (this.answerIndex < this.answerMap.length) {
|
||||
this.currentMessage[mode] += this.answerMap[this.answerIndex++];
|
||||
// this.$nextTick(() => {
|
||||
this.timer = requestAnimationFrame(renderChar);
|
||||
// });
|
||||
} else {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
};
|
||||
|
||||
this.timer = requestAnimationFrame(renderChar);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
107
vue.config.js
107
vue.config.js
@@ -1,42 +1,48 @@
|
||||
const autoprefixer = require('autoprefixer')
|
||||
const pxtoviewport = require('postcss-px-to-viewport')
|
||||
const path = require('path')
|
||||
const autoprefixer = require('autoprefixer');
|
||||
const pxtoviewport = require('postcss-px-to-viewport');
|
||||
const path = require('path');
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir)
|
||||
return path.join(__dirname, dir);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
publicPath: '/',
|
||||
lintOnSave: false, //是否开启代码检查
|
||||
outputDir: 'dist', //打包输出目录
|
||||
lintOnSave: false,
|
||||
outputDir: 'dist',
|
||||
productionSourceMap: false,
|
||||
devServer: {
|
||||
https: false,
|
||||
host: "0.0.0.0",
|
||||
disableHostCheck: true
|
||||
host: '0.0.0.0',
|
||||
disableHostCheck: true,
|
||||
},
|
||||
css: {
|
||||
sourceMap: true, // 查看css属于哪个css文件
|
||||
loaderOptions: {
|
||||
sourceMap: true,
|
||||
loaderOptions: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
autoprefixer(),
|
||||
pxtoviewport({
|
||||
viewportWidth: 375,
|
||||
// 该项仅在使用 Circle 组件时需要
|
||||
// 原因参见 https://github.com/youzan/vant/issues/1948
|
||||
selectorBlackList: ['van-circle__layer']
|
||||
})
|
||||
]
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
autoprefixer(), // 自动加浏览器前缀
|
||||
pxtoviewport({ // px 转 viewport
|
||||
viewportWidth: 375,
|
||||
selectorBlackList: ['van-circle__layer'] // 排除vant circle组件样式转换
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
chainWebpack: (config) => {
|
||||
// 移除 prefetch 插件
|
||||
config.resolve.alias.set('@utils', resolve('./src/assets/js/utils'))
|
||||
// 设置路径别名
|
||||
config.resolve.alias
|
||||
.set('@', resolve('src'))
|
||||
.set('@utils', resolve('src/assets/js/utils'));
|
||||
|
||||
// config.plugins.delete('prefetch')
|
||||
/* 配置svg图标自动加载 begin */
|
||||
config.module.rule('svg').exclude.add(resolve('src/icons')).end()
|
||||
// 移除 prefetch 插件(减少初始加载体积)
|
||||
config.plugins.delete('prefetch');
|
||||
|
||||
// SVG 图标配置
|
||||
config.module.rule('svg').exclude.add(resolve('src/icons')).end();
|
||||
config.module
|
||||
.rule('icons')
|
||||
.test(/\.svg$/)
|
||||
@@ -45,8 +51,10 @@ module.exports = {
|
||||
.use('svg-sprite-loader')
|
||||
.loader('svg-sprite-loader')
|
||||
.options({
|
||||
symbolId: 'icon-[name]'
|
||||
})
|
||||
symbolId: 'icon-[name]',
|
||||
});
|
||||
|
||||
// 处理 .mjs 文件(用于 mermaid 等模块)
|
||||
config.module
|
||||
.rule('mjs')
|
||||
.test(/\.mjs$/)
|
||||
@@ -54,20 +62,37 @@ module.exports = {
|
||||
.end()
|
||||
.use('babel-loader')
|
||||
.loader('babel-loader');
|
||||
//设置路径别名
|
||||
|
||||
// 添加对现代 JS 的支持(如 ?? 和 ?.)
|
||||
config.module
|
||||
.rule('js')
|
||||
.test(/\.js$/)
|
||||
.use('babel-loader')
|
||||
.loader('babel-loader')
|
||||
.options({
|
||||
presets: ['@babel/preset-env'],
|
||||
});
|
||||
},
|
||||
configureWebpack: (config) => {
|
||||
;(config.devtool = 'source-map'), // 调试js
|
||||
(config.performance = {
|
||||
hints: 'error',
|
||||
//入口起点的最大体积 700kb
|
||||
maxEntrypointSize: 7168000,
|
||||
//生成文件的最大体积 700kb
|
||||
maxAssetSize: 7168000,
|
||||
//只给出 js 文件的性能提示
|
||||
assetFilter: function (assetFilename) {
|
||||
return assetFilename.endsWith('.js')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// 强制使用 CommonJS 模块加载 mermaid
|
||||
config.resolve = config.resolve || {};
|
||||
config.resolve.alias = config.resolve.alias || {};
|
||||
// config.resolve.alias.mermaid$ = path.resolve(
|
||||
// __dirname,
|
||||
// './node_modules/mermaid/dist/mermaid.common.js'
|
||||
// );
|
||||
|
||||
// Webpack devtool 设置
|
||||
config.devtool = 'source-map';
|
||||
|
||||
// 打包性能提示设置
|
||||
config.performance = {
|
||||
hints: 'error',
|
||||
maxEntrypointSize: 7168000, // 7MB
|
||||
maxAssetSize: 7168000, // 7MB
|
||||
assetFilter: function (assetFilename) {
|
||||
return assetFilename.endsWith('.js');
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user