feat(AI): 优化聊天组件功能和交互

- 新增消息状态管理,改善发送和接收消息的用户体验
- 优化语音录制和转换功能
- 调整消息渲染逻辑,支持流式响应
- 重构部分代码以提高可维护性
- 调整界面样式以提升视觉效果
This commit is contained in:
2025-08-11 11:54:23 +08:00
parent 76d8187720
commit 9c6717e94f
5 changed files with 73 additions and 59 deletions

View File

@@ -13,11 +13,18 @@
<footer class="chat-footer pt10 pb10">
<!-- 输入框 or 按住说话提示 -->
<div class="input-wrapper">
<input v-if="!isVoiceMode" type="text" v-model="newMessage" placeholder="你有什么想知道的?"
@keyup.enter="beforeSend" />
<div v-else class="voice-hint-container" :class="{ disabled: messageStatus === 'send' }"
@mousedown="startRecording" @selectstart="() => false" @mouseup="stopRecording" @mouseleave="stopRecording"
@touchend="stopRecording" @touchstart="startRecording">
<input v-if="!isVoiceMode" type="text" v-model="newMessage" placeholder="你有什么想知道的?" @keyup.enter="beforeSend" />
<div
v-else
class="voice-hint-container"
:class="{ disabled: messageStatus === 'send' }"
@mousedown="startRecording"
@selectstart="() => false"
@mouseup="stopRecording"
@mouseleave="stopRecording"
@touchend="stopRecording"
@touchstart="startRecording"
>
<div class="waveform" :class="{ active: isRecording }">
<span class="bar"></span>
<span class="bar"></span>
@@ -28,8 +35,7 @@
<!--<div class="hint-text" v-if='!isRecording'>按住说话</div>-->
</div>
<!-- 发送按钮 -->
<button @click="beforeSend" :disabled="messageStatus === 'send'"
:class="{ disabled: messageStatus === 'send' }" class="ml10 active send">
<button @click="beforeSend" :disabled="messageStatus === 'send'" :class="{ disabled: messageStatus === 'send' }" class="ml10 active send">
发送
</button>
<!-- <button @click="cancelSend">取消</button> -->
@@ -48,43 +54,48 @@
import SvgIcon from '@/components/svg-icon/index.vue'
import { audioToText, gwcsChat } from '@/api/generatedApi'
const MESSAGE_STATUS = {
processing: 1,
end: 0
}
export default {
components: {
SvgIcon,
SvgIcon
},
props: {
messages: {
type: Array,
default: () => [],
default: () => []
},
messageStatus: {
type: String,
default: 'stop',
default: 'stop'
},
isDeep: {
type: Boolean,
default: false,
default: false
},
isSearching: {
type: Boolean,
default: false,
default: false
},
conversationId: {
type: String,
default: '',
default: ''
},
productName: {
type: String,
default: '',
default: ''
},
autoScrollEnabled: {
type: Boolean,
default: false,
default: false
},
chatData: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
// action: {
// type: String,
// default: 'normal_chat',
@@ -109,24 +120,31 @@ export default {
genMessage: null,
messageInfo: {
is_complete: false.toString(),
information: '',
information: ''
},
currentMessageID: "",
currentMessageID: '',
// 打字机相关
typingText: '',
typingQueue: [],
typingQueueText: [],
isTyping: false,
typingSpeed: 30,
typingTimeout: null,
typingTimeout: null
}
},
watch: {
isTyping(value) {
// 通过 typing 控制发送的状态
// 通过 typing 控制发送的状态
// console.log(`typing status change `, value)
if (!value) this.$emit('update:messageStatus', 'stop')
else this.$emit('update:messageStatus', 'send')
if (!value) {
this.$emit('update:messageStatus', 'stop')
this.currentMessage.status = MESSAGE_STATUS.end
this.genMessage.status = MESSAGE_STATUS.end
} else {
this.$emit('update:messageStatus', 'send')
this.currentMessage.status = MESSAGE_STATUS.processing
this.genMessage.status = MESSAGE_STATUS.processing
}
}
},
methods: {
@@ -151,7 +169,7 @@ export default {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
this.mediaRecorder = new MediaRecorder(stream)
this.audioChunks = []
this.mediaRecorder.ondataavailable = (event) => {
this.mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
this.audioChunks.push(event.data)
}
@@ -195,12 +213,12 @@ export default {
formData.append('appType', 'haslBigHelper')
formData.append('user', 'chenyuda')
audioToText(formData)
.then((res) => {
.then(res => {
if (res) {
resolve(res.content)
}
})
.catch((err) => {
.catch(err => {
reject(err)
})
})
@@ -215,25 +233,26 @@ export default {
// this.typingQueue = []
this.currentMessage = JSON.parse(JSON.stringify(this.messageInfo))
let params = {
appType: "gwcsHelper",
appType: 'gwcsHelper',
conversationId: this.conversationId,
message: JSON.stringify(this.messageInfo),
user: "gwcs-test",
inputs: {},
user: 'gwcs-test',
inputs: {}
}
this.currentMessage = {
...this.messageInfo,
status: MESSAGE_STATUS.processing,
type: 'bot',
text: '',
think: '',
isThink: false,
showThink: false,
isLike: false,
isDisLike: false,
isDisLike: false
}
if (this.single) {
// this.$set(this.messages, this.messages.length - 1, { ...this.currentMessage })
this.$set(this.messages, this.messages.length - 1, this.genMessage = { ...this.currentMessage })
this.$set(this.messages, this.messages.length - 1, (this.genMessage = { ...this.currentMessage }))
} else {
this.messages.push(this.currentMessage)
}
@@ -249,13 +268,13 @@ export default {
headers: { 'Content-Type': 'application/json' },
signal: abortController.signal,
body: JSON.stringify(params),
timeout: 60000,
timeout: 60000
})
.then(async (res) => {
.then(async res => {
await this.processStreamResponse(res, this.requestIndex)
this.single = false
})
.catch((err) => {
.catch(err => {
// debugger
this.$emit('update:messageStatus', 'stop')
})
@@ -270,15 +289,15 @@ export default {
let buffer = ''
while (true) {
// try {
const { done, value } = await reader.read()
if (done) break
buffer += new TextDecoder().decode(value)
const lines = buffer.split('\n')
lines.slice(0, -1).forEach((line) => {
const parsed = this.parseStreamLine(line)
if (parsed) this.updateMessageContent(parsed, requestIndex)
})
buffer = lines[lines.length - 1] || ''
const { done, value } = await reader.read()
if (done) break
buffer += new TextDecoder().decode(value)
const lines = buffer.split('\n')
lines.slice(0, -1).forEach(line => {
const parsed = this.parseStreamLine(line)
if (parsed) this.updateMessageContent(parsed, requestIndex)
})
buffer = lines[lines.length - 1] || ''
// } catch (error) {
// console.error('读取流数据时发生错误:', error)
// break
@@ -391,9 +410,8 @@ export default {
// console.log('requestIndex === 2', char);
this.$set(this.genMessage, 'text', this.genMessage.text + char)
// this.messages.splice(this.messages.length - 1, 1, this.genMessage)
} if (requestIndex === 1) {
// console.log('requestIndex === 1', char);
}
if (requestIndex === 1) {
this.$set(this.currentMessage, 'text', this.currentMessage.text + char)
}
const delay = this.getTypingDelay(char)
@@ -446,7 +464,7 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
background-color: #fafafa;
display: flex;
&>div {
& > div {
margin: 0 auto;
background-color: #fff;
width: 90%;
@@ -498,7 +516,8 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
cursor: pointer;
}
.send {}
.send {
}
.active {
background-color: $primary-color;
@@ -544,7 +563,7 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
display: grid;
grid-template-columns: 1fr 50px;
&>button {
& > button {
// width: 50px;
color: $primary-color;
}
@@ -667,7 +686,6 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
}
@keyframes wave-animation {
0%,
100% {
transform: scaleY(1);

View File

@@ -13,8 +13,7 @@
<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" />
@@ -29,7 +28,7 @@
</span>
<div class="text-right fs12 mb5 mr10" style="font-size: 10px; color: #f6aa21">
<!-- 新增点赞和踩按钮 -->
<div class="reaction-buttons mb10" v-if="message.type !== 'user' && messageStatus === 'stop'">
<div class="reaction-buttons mb10" v-if="message.type !== 'user' && !message.status">
<button @click="handleReaction(message, 'like')" class="like">
<svg-icon :icon-class="message.isLike ? 'fillLike' : 'like'" class-name="chat-icon"></svg-icon>
</button>
@@ -50,7 +49,6 @@ import { Icon } from 'vant'
import TreasureBox from '@/views/AI/components/treasureBox.vue'
import { md } from './js/markdown-it'
export default {
name: 'message',
components: { TreasureBox, [Icon.name]: Icon },
@@ -81,8 +79,6 @@ export default {
methods: {
render(message) {
const text = this.filterVisible(message)
// console.log(`text`, text);
return md.render(text)
},
setProductName(e) {

View File

@@ -23,7 +23,7 @@
<van-icon name="upgrade" size="2em" @click="scrollToTop" />
</div> -->
</main>
<chat-entry :ignore-list="['1']" />
<chat-entry :ignore-list="['1']" v-if="messages.length === 0"/>
<chat-message ref="chatMessage" :messages.sync="messages" :messageStatus.sync="messageStatus" :is-deep.sync="isDeep"
:conversation-id.sync="conversationId" :is-searching.sync="isSearching" :product-name.sync="productName"

View File

@@ -18,7 +18,7 @@
<van-icon name="upgrade" size="2em" @click="scrollToTop" />
</div>
</main>
<ChatEntry :ignoreList="['3']" />
<!-- <ChatEntry :ignoreList="['3']" /> -->
<chatMessage :messages.sync="messages" :messageStatus.sync="messageStatus" :is-deep.sync="isDeep"
:conversation-id.sync="conversationId" :is-searching.sync="isSearching" :product-name.sync="productName"
:autoScrollEnabled.sync="autoScrollEnabled" @getIsThink="getIsThink" action="chat"

View File

@@ -18,7 +18,7 @@
<van-icon name="upgrade" size="2em" @click="scrollToTop" />
</div>
</main>
<ChatEntry :ignoreList="['2']" />
<!-- <ChatEntry :ignoreList="['2']" /> -->
<chatMessage :messages.sync="messages" :messageStatus.sync="messageStatus" :is-deep.sync="isDeep"
:conversation-id.sync="conversationId" :is-searching.sync="isSearching" :product-name.sync="productName"