feat(AI-new): 优化聊天组件并添加新功能

- 重构 chat-new 组件,优化输入框和发送按钮逻辑
- 新增单次请求管控功能,改进多次请求处理
- 优化消息渲染逻辑,处理未闭合标签问题
- 调整卡片样式,增加新图标
- 更新路由配置,修改页面标题
This commit is contained in:
huangzhe
2025-07-29 19:53:56 +08:00
parent b873e378d2
commit 05144d9afb
14 changed files with 157 additions and 114 deletions

View File

@@ -14,7 +14,7 @@
<!-- 输入框 or 按住说话提示 -->
<div class="input-wrapper">
<input v-if="!isVoiceMode" type="text" v-model="newMessage" placeholder="你有什么想知道的?"
@keyup.enter="sendMessage" />
@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">
@@ -25,11 +25,11 @@
<span class="bar"></span>
<span class="bar"></span>
</div>
<!-- <div class="hint-text" v-if='!isRecording'>按住说话</div>-->
<!--<div class="hint-text" v-if='!isRecording'>按住说话</div>-->
</div>
<!-- 发送按钮 -->
<button @click="sendMessage" :disabled="messageStatus === 'send'"
:class="{ disabled: messageStatus === 'send' }" class="active send">
<button @click="beforeSend" :disabled="messageStatus === 'send'"
:class="{ disabled: messageStatus === 'send' }" class="ml10 active send">
发送
</button>
</div>
@@ -39,24 +39,13 @@
<span v-else class="ml15 mr5 input-icon"></span> -->
</button>
</footer>
<!-- <section class="section pb10" > -->
<!-- <button @click="deepInternet" :class="{ active: isDeep }">
<svg-icon icon-class="think" class-name="chat-icon"></svg-icon>
深度思考
</button> -->
<!-- 发送按钮 -->
<!-- <button @click="sendMessage" :disabled="messageStatus === 'send'"
:class="{ disabled: messageStatus === 'send' }" class="mr10 active send fs16">
发送
</button> -->
<!-- </section> -->
</div>
</div>
</template>
<script>
import SvgIcon from '@/components/svg-icon/index.vue'
import { audioToText, gwcsChat, chatProduct } from '@/api/generatedApi'
import { audioToText, gwcsChat } from '@/api/generatedApi'
export default {
components: {
@@ -95,10 +84,16 @@ export default {
type: Object,
default: () => ({}),
},
// action: {
// type: String,
// default: 'normal_chat',
// }
},
data() {
return {
requestSingle: undefined,
// 管控单次请求,当 abort 之后, while 后面的会进行重复请求
single: false,
isThink: false,
newMessage: '',
isRecording: false,
@@ -108,7 +103,10 @@ export default {
isVoiceMode: false,
answerMap: '',
currentMessage: null,
messageInfo: {
is_complete: false.toString(),
information: '',
},
// 打字机相关
typingText: '',
typingQueue: [],
@@ -118,6 +116,14 @@ export default {
typingTimeout: null,
}
},
watch: {
isTyping(value) {
// 通过 typing 控制发送的状态
// console.log(`typing status change `, value)
if (!value) this.$emit('update:messageStatus', 'stop')
else this.$emit('update:messageStatus', 'send')
}
},
methods: {
deepInternet() {
this.$emit('update:isDeep', !this.isDeep)
@@ -152,19 +158,19 @@ export default {
console.error(err)
}
},
hasTreasureBox() {
chatProduct({ query: this.newMessage })
.then((res) => {
if (res) {
this.messageStatus = 'stop'
this.messages.push({ type: 'box', text: this.newMessage, detail: res.content })
this.newMessage = ''
}
})
.catch(() => {
this.messageStatus = 'stop'
})
},
// hasTreasureBox() {
// chatProduct({ query: this.newMessage })
// .then((res) => {
// if (res) {
// this.messageStatus = 'stop'
// this.messages.push({ type: 'box', text: this.newMessage, detail: res.content })
// this.newMessage = ''
// }
// })
// .catch(() => {
// this.messageStatus = 'stop'
// })
// },
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop()
@@ -207,21 +213,22 @@ export default {
axiosGetAiChat() {
const abortController = new AbortController()
this.requestSingle = abortController
let message = {
is_complete: false.toString(),
information: this.newMessage,
}
this.messageInfo.information = this.single ? this.messageInfo.information : this.newMessage
this.currentMessage = JSON.parse(JSON.stringify(message))
// 重置 answerMap
this.answerMap = ''
this.currentMessage = JSON.parse(JSON.stringify(this.messageInfo))
let params = {
appType: "gwcsHelper",
conversationId: "",
message: JSON.stringify(message),
message: JSON.stringify(this.messageInfo),
user: "gwcs-test",
inputs: {}
inputs: {},
// action: this.action
}
this.currentMessage = {
...message,
...this.messageInfo,
type: 'bot',
text: '',
think: '',
@@ -230,28 +237,37 @@ export default {
isLike: false,
isDisLike: false,
}
this.messages.push(this.currentMessage)
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)
}
// 如果有自定义参数
if (this.chatData) {
for (let k in this.chatData) {
params[k] = this.chatData[k]
}
}
if (this.$route.query.compareId) {
params.compareResult = JSON.parse(sessionStorage.getItem('results'))
}
// if (this.$route.query.compareId) {
// params.compareResult = JSON.parse(sessionStorage.getItem('results'))
// }
this.newMessage = ''
fetch(gwcsChat(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: abortController.signal,
body: JSON.stringify(params),
timeout: 60000,
})
.then(async (res) => {
this.newMessage = ''
await this.processStreamResponse(res)
this.single = false
})
.catch((err) => {
// debugger
this.$emit('update:messageStatus', 'stop')
})
},
@@ -264,6 +280,8 @@ export default {
const reader = response.body.getReader()
let buffer = ''
while (true) {
// if (this.single) break
try {
const { done, value } = await reader.read()
if (done) break
@@ -286,12 +304,30 @@ export default {
if (!cleanLine) return null
const data = JSON.parse(cleanLine)
// console.log(data)
// console.log(data)
if (data.answer) {
this.answerMap += data.answer
const match = /<is_complete>([^<]*)(?:<\/is_complete>)?/.exec(data.answer)
if (match && match[1] === 'true') {
const is_complete = /<is_complete>([^<]*)(?:<\/is_complete>)?/.exec(this.answerMap)
const information = /<information>([^<]*)(?:<\/information>)?/.exec(this.answerMap)
const text = /<text>([^<]*)(?:<\/text>)?/.exec(this.answerMap)
// console.log(`is_complete, information, text`, is_complete, information, text)
// const isCompleteRes = is_complete.some(item => item === 'true')
this.messageInfo.information = information ? information[1].trim() : this.newMessage
this.messageInfo.is_complete = is_complete ? is_complete[1].trim() : 'false'
if (is_complete && is_complete[1] === 'true' && text && text[1].trim() === '') {
// alert("message end")
this.requestSingle.abort()
// setTimeout(() => {
// debugger
// this.$emit('update:messageStatus', 'send')
this.single = true
// debugger
this.axiosGetAiChat()
// }, 1000)
}
}
@@ -324,9 +360,10 @@ export default {
},
updateMessageContent(parse) {
let { event, answer, isThink } = parse
if (event === 'message_end') {
this.$emit('update:messageStatus', 'stop')
}
// 会导致发送按钮提前高亮展示
// if (event === 'message_end') {
// this.$emit('update:messageStatus', 'stop')
// }
if (!this.currentMessage || !answer) return
if (event !== 'message') return
// console.log(parse);
@@ -342,7 +379,6 @@ export default {
},
startTypingAnimation() {
this.isTyping = true
const typeNextChar = () => {
if (this.typingQueue.length === 0) {
this.isTyping = false
@@ -353,8 +389,7 @@ export default {
const chunk = this.typingQueue.shift()
// console.log(this.messages);
const chars = /* chunk.answer.split('') */ Array.from(chunk.answer)
// console.log(chars);
const chars = Array.from(chunk.answer)
const isThink = chunk.isThink
// 内部递归函数,用于逐字输出当前块
@@ -365,6 +400,7 @@ 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)
@@ -381,26 +417,23 @@ export default {
}
return this.typingSpeed
},
sendMessage() {
beforeSend() {
if (this.messageStatus === 'send') return
if (this.newMessage.trim() === '') return
this.newMessage = this.newMessage.replace(/<[^>]+>/g, '')
this.messages.push({ type: 'user', text: this.newMessage })
this.$emit('update:messageStatus', 'send')
if (this.newMessage.includes('工具箱')) {
this.hasTreasureBox()
return
}
this.$emit('update:autoScrollEnabled', true)
this.axiosGetAiChat()
this.sendMessage()
},
cellClick(item) {
this.newMessage = item.title
this.messages.push({ type: 'user', text: this.newMessage })
this.sendMessage()
},
sendMessage() {
this.$emit('update:messageStatus', 'send')
this.$emit('update:autoScrollEnabled', true)
this.axiosGetAiChat()
}
},
}
}
</script>
@@ -517,7 +550,7 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
}
input {
width: 110%;
width: 100%;
padding: 10px;
border: none;
background: #fff;