mirror of
http://112.124.100.131/ebiz-ai/ebiz-base-ai.git
synced 2025-12-06 17:36:48 +08:00
feat(AI): 优化聊天组件功能和交互
- 新增消息状态管理,改善发送和接收消息的用户体验 - 优化语音录制和转换功能 - 调整消息渲染逻辑,支持流式响应 - 重构部分代码以提高可维护性 - 调整界面样式以提升视觉效果
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user