feat: 新增AI聊天界面及消息组件,支持语音输入和markdown渲染

This commit is contained in:
huangzhe
2025-07-30 14:49:10 +08:00
parent 4d7d9dd0d8
commit 87d458ce83
4 changed files with 34 additions and 13 deletions

View File

@@ -1,10 +1,13 @@
.card {
border-radius: 5px;
margin: 10px 0;
padding: 5px 10px;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 5%;
flex-shrink: 0;
box-shadow: 0 0 10px rgba(0, 0, 0, .05);
}
.card > div {
@@ -30,15 +33,21 @@
}
.card > section > :nth-child(2) > div {
font-size: 12px;
background-color: #0065ff;
padding: 1px 4px;
border-radius: 3px;
padding: 0px 3px;
border-radius: 5px;
}
/* 选择遇到的第一个 div */
.render-container > div:nth-of-type(1) > img {
width: 100%;
background-color: red;
/* background-color: red; */
}
.render-container > ul {
/* background-color: red; */
list-style: none !important;
}
.render-container > div {

View File

@@ -32,6 +32,7 @@
:class="{ disabled: messageStatus === 'send' }" class="ml10 active send">
发送
</button>
<!-- <button @click="cancelSend">取消</button> -->
</div>
<!-- 语音按钮按住说话 -->
<button @click="isVoiceMode = !isVoiceMode" class="mic-button ml10 mr10" v-if="false">
@@ -218,7 +219,7 @@ export default {
// 重置 answerMap
this.answerMap = ''
this.typingQueue = []
this.currentMessage = JSON.parse(JSON.stringify(this.messageInfo))
let params = {
appType: "gwcsHelper",
@@ -241,7 +242,6 @@ export default {
if (this.single) {
// this.messages[this.messages.length - 1]
this.messages.splice(this.messages.length - 1, 1, this.currentMessage)
} else {
this.messages.push(this.currentMessage)
@@ -301,6 +301,7 @@ export default {
},
parseStreamLine(line) {
try {
const cleanLine = line.replace(/^data:\s*/, '')
if (!cleanLine) return null
const data = JSON.parse(cleanLine)
@@ -373,6 +374,7 @@ export default {
answer: answer,
isThink: isThink,
}
// debugger
this.typingQueue.push(chars)
if (!this.isTyping) {
this.startTypingAnimation(mode)
@@ -401,7 +403,6 @@ export default {
return
}
const char = chars.shift() || ''
// if (this.single)
this.$set(this.currentMessage, isThink ? 'think' : 'text', this.currentMessage[isThink ? 'think' : 'text'] + char)
const delay = this.getTypingDelay(char)
setTimeout(outputChar, delay)
@@ -435,6 +436,10 @@ export default {
this.$emit('update:autoScrollEnabled', true)
this.axiosGetAiChat()
},
cancelSend() {
this.requestSingle.abort()
}
}
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<section>
<!-- {{ messagesList }} -->
{{ messagesList }}
<div v-for="(message, index) in messagesList" :key="index" class="message-item">
<!-- {{ message }} -->
<!--用户消息-->
@@ -14,7 +14,8 @@
<span class="speakLoadingToast pv10">
<van-loading type="spinner" :color="primaryColor" size="20px" v-if="!message.text" />
<span class="loading-text">{{ message.text ? '思考完成' : '思考中...' }}</span>
<van-icon name="arrow-up" :class="message.showThink ? 'rate360' : 'rate180'" @click="showThink(message)" v-if="message.think" />
<van-icon name="arrow-up" :class="message.showThink ? 'rate360' : 'rate180'" @click="showThink(message)"
v-if="message.think" />
</span>
<!--开启思考-->
<p v-html="md.render(message.think)" v-if="message.think && message.showThink" class="thinkText" />
@@ -90,13 +91,15 @@ export default {
if (new RegExp('^[\u4e00-\u9fa5]+', 'g').test(text)) return text
// 捕获 不包含 < 的后置标签 span> /span> a</span>
text = text.replace(/^[\/]?[a-zA-z0-9]+[</\w>]+/g, '')
text = text.replace(/^[/]?[a-zA-z0-9]+[</\w>]+/g, '')
// 尝试匹配 </abc> 标签
text = text.replace(/^<\/\w+>/g, '')
text = text.replace(/^<\w+>/g, '')
// 尝试把 </ 标签去除
text = text.replace(/[<\/]+$/g, '')
console.log(text);
// text = text.replace(/[</]+$/g, '')
text = text.replace(/<[^>]*>/g, '')
// console.log(text);
return text
}
// 只把 text 标签内容渲染

View File

@@ -14,6 +14,7 @@
:is-search="isSearching"
:think-ok="isThink"
@setProductName="setProductName"
@reload-message="reloadMessage"
v-if="messages.length > 1"
></messageComponent>
<div class="mb20" v-else>
@@ -137,6 +138,9 @@ export default {
},
initBotMessage() {
this.messages = []
},
reloadMessage({message,index}){
this.$refs.chatMessage.reloadMessage({message,index})
}
},
watch: {