diff --git a/src/api/generatedApi/index.js b/src/api/generatedApi/index.js
index 2f1a3dd..0b5d34e 100644
--- a/src/api/generatedApi/index.js
+++ b/src/api/generatedApi/index.js
@@ -36,3 +36,14 @@ export function chat(data) {
return getUrl('/hasl/chat')
}
+
+
+export function audioToText(data) {
+ //聊天获取产品百宝箱
+ // 聊天框输入内容以“百宝箱”结尾时,调用这个接口
+ return request({
+ url: getUrl('/chat/audio-to-text'),
+ method: 'post',
+ data,
+ })
+}
diff --git a/src/views/AI/index.vue b/src/views/AI/index.vue
index 064b5e4..d209430 100644
--- a/src/views/AI/index.vue
+++ b/src/views/AI/index.vue
@@ -7,12 +7,9 @@
-
-
-
-
-
@@ -41,7 +78,7 @@ import { Icon } from 'vant'
import messageComponent from './components/message.vue'
import SvgIcon from '@/components/svg-icon/index.vue'
import HotProducts from '@/views/AI/components/HotProducts.vue'
-import { chat, chatProduct } from '@/api/generatedApi'
+import { chat, chatProduct,audioToText } from '@/api/generatedApi'
export default {
components: {
@@ -50,7 +87,6 @@ export default {
messageComponent,
HotProducts,
},
-
data() {
return {
productName:'',
@@ -67,9 +103,15 @@ export default {
isDeep: false,
autoScrollEnabled: true,
scrollPosition: 0,
+ // 录音相关状态
+ isRecording: false,
+ mediaRecorder: null,
+ audioChunks: [],
+ isRecognizing: false,
+ // 语音模式开关
+ isVoiceMode: false,
}
},
-
methods: {
deepInternet() {
this.isDeep = !this.isDeep
@@ -82,38 +124,29 @@ export default {
this.conversationId = ''
this.productName = ''
},
-
hasTreasureBox() {
chatProduct({ query: this.newMessage }).then((res) => {
if (res) {
this.messageStatus = 'stop'
- this.messages.push({ type: 'box', text: this.newMessage,detail:res.content })
+ this.messages.push({ type: 'box', text: this.newMessage, detail: res.content })
this.newMessage = ''
}
- }).catch(()=>{
+ }).catch(() => {
this.messageStatus = 'stop'
})
},
-
sendMessage() {
- if(this.messageStatus === 'send'){
- return
- }
+ if (this.messageStatus === 'send') return
if (this.newMessage.trim() === '') return
-
this.messages.push({ type: 'user', text: this.newMessage })
this.messageStatus = 'send'
-
if (this.newMessage.includes('工具箱')) {
this.hasTreasureBox()
return
}
this.autoScrollEnabled = true
this.axiosGetAiChat()
-
-
},
-
throttle(fn, delay = 50) {
let lastCall = 0
return (...args) => {
@@ -124,15 +157,14 @@ export default {
}
}
},
-
- scrollToTop(){
+ scrollToTop() {
const messageArea = this.$refs.messageArea
- messageArea.scrollTop = 0
+ if (messageArea) {
+ messageArea.scrollTop = 0
+ }
},
-
- scrollToBottom: function () {
+ scrollToBottom() {
if (!this.autoScrollEnabled) return
-
this.$nextTick(() => {
const messageArea = this.$refs.messageArea
if (messageArea) {
@@ -140,22 +172,81 @@ export default {
}
})
},
- setProductName(e){
+ setProductName(e) {
console.log(e)
},
handleScroll() {
const messageArea = this.$refs.messageArea
if (!messageArea) return
-
const threshold = 10
- const isAtBottom = messageArea.scrollHeight - messageArea.clientHeight <= messageArea.scrollTop + threshold
+ const isAtBottom =
+ messageArea.scrollHeight - messageArea.clientHeight <=
+ messageArea.scrollTop + threshold
this.autoScrollEnabled = isAtBottom
this.scrollPosition = messageArea.scrollTop
},
-
+ async startRecording() {
+ if (this.isRecognizing) return
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
+ this.mediaRecorder = new MediaRecorder(stream)
+ this.audioChunks = []
+ this.mediaRecorder.ondataavailable = event => {
+ if (event.data.size > 0) {
+ this.audioChunks.push(event.data)
+ }
+ }
+ this.mediaRecorder.onstop = () => {
+ this.handleStopRecording()
+ }
+ this.mediaRecorder.start()
+ this.isRecording = true
+ } catch (err) {
+ alert('无法访问麦克风,请检查权限')
+ console.error(err)
+ }
+ },
+ stopRecording() {
+ if (this.mediaRecorder && this.isRecording) {
+ this.mediaRecorder.stop()
+ this.isRecording = false
+ }
+ },
+ async handleStopRecording() {
+ this.isRecognizing = true
+ const blob = new Blob(this.audioChunks, { type: 'audio/webm' })
+ try {
+ const text = await this.callVoiceRecognitionAPI(blob)
+ console.log(text)
+ if (text) {
+ this.newMessage = text
+ this.messageStatus = 'stop'
+ this.sendMessage()
+ }
+ } catch (error) {
+ console.error('语音识别失败:', error)
+ this.newMessage = ''
+ } finally {
+ this.isRecognizing = false
+ }
+ },
+ callVoiceRecognitionAPI(blob) {
+ return new Promise((resolve, reject) => {
+ const formData = new FormData();
+ formData.append('file', blob);
+ formData.append('appType', 'haslBigHelper');
+ formData.append('user', 'chenyuda');
+ audioToText(formData).then(res => {
+ if(res){
+ resolve(res.content)
+ }
+ }).catch(err => {
+ reject(err)
+ })
+ })
+ },
axiosGetAiChat() {
const abortController = new AbortController()
-
this.currentMessage = JSON.parse(
JSON.stringify({
type: 'bot',
@@ -168,55 +259,46 @@ export default {
})
)
this.messages.push(this.currentMessage)
-
const params = {
query: this.newMessage,
isDeep: this.isDeep ? 1 : 0,
isOnline: this.isSearching ? 1 : 0,
user: 'chenyuda',
conversationId: this.conversationId,
- productName:this.productName,
+ productName: this.productName,
}
-
fetch(chat(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
signal: abortController.signal,
body: JSON.stringify(params),
})
- .then(async (res) =>{
+ .then(async (res) => {
this.newMessage = ''
- await this.processStreamResponse(res)
+ await this.processStreamResponse(res)
})
.catch((err) => {
this.messageStatus = 'stop'
})
},
-
async processStreamResponse(response) {
if (!response.ok) throw new Error(`HTTP错误: ${response.status}`)
-
if (!response.body) {
console.error('响应体不存在:', response)
return
}
-
const reader = response.body.getReader()
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)
})
-
buffer = lines[lines.length - 1] || ''
} catch (error) {
console.error('读取流数据时发生错误:', error)
@@ -224,17 +306,14 @@ export default {
}
}
},
-
parseStreamLine(line) {
try {
const cleanLine = line.replace(/^data:\s*/, '')
if (!cleanLine) return null
const data = JSON.parse(cleanLine)
- if(data.answer){
- this.answerMap+=data.answer
+ if (data.answer) {
+ this.answerMap += data.answer
}
-
-
this.updateConversationState(data)
return data
} catch (error) {
@@ -243,48 +322,20 @@ export default {
}
},
updateConversationState(data) {
- this.conversationId = data.conversation_id || conversationId.value
- if (data.answer && data.answer.indexOf('') !== -1) {
- this.isThink = true
- }
-
- if (data.answer && data.answer.indexOf('') !== -1) {
+ this.conversationId = data.conversation_id || this.conversationId
+ if (data.answer && data.answer.indexOf('withTo') !== -1) {
this.isThink = false
}
},
updateMessageContent({ answer, event }) {
- if(event === 'message_end'){
+ if (event === 'message_end') {
this.messageStatus = 'stop'
}
- if (!this.currentMessage || !answer) return;
-
- const mode = this.isThink ? 'think' : 'text';
-
+ if (!this.currentMessage || !answer) return
+ const mode = this.isThink ? 'think' : 'text'
this.currentMessage[mode] += answer
- return
- // 清除之前的动画任务
- 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: {
messages: {
handler() {
@@ -305,6 +356,10 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
display: flex;
flex-direction: column;
height: 100vh;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
.chat-main {
flex: 1;
@@ -312,12 +367,14 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
padding: 10px;
background: #f7f8fa;
position: relative;
- .button-container{
+
+ .button-container {
position: fixed;
bottom: 120px;
- right:10px;
+ right: 10px;
border-radius: 50%;
}
+
.chat-content {
height: 100%;
.message-area {
@@ -358,13 +415,88 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
padding: 10px;
background-color: #fff;
- input {
+ .input-wrapper {
flex: 1;
- padding: 10px;
- border: none;
- background: #f5f5f5;
- border-radius: 5px;
margin-right: 10px;
+
+ input {
+ width: 100%;
+ padding: 10px;
+ border: none;
+ background: #f5f5f5;
+ border-radius: 5px;
+ font-size: 14px;
+ }
+
+ .voice-hint-container {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 10px;
+ background-color: #f5f5f5;
+ border-radius: 5px;
+ color: #888;
+ font-size: 14px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ &:active {
+ background-color: #eaeaea;
+ }
+
+ .waveform {
+ display: flex;
+ align-items: flex-end;
+ align-items: center;
+ //height: 30px;
+ //width: 60px;
+ gap: 2px;
+
+ &.active .bar {
+ animation: wave-animation 1s infinite ease-in-out;
+ }
+
+ .bar {
+ width: 4px;
+ background-color: $primary-color;
+ margin: 0 1px;
+
+ &:nth-child(1) { height: 10px; animation-delay: 0s; }
+ &:nth-child(2) { height: 16px; animation-delay: 0.1s; }
+ &:nth-child(3) { height: 12px; animation-delay: 0.2s; }
+ &:nth-child(4) { height: 18px; animation-delay: 0.3s; }
+ &:nth-child(5) { height: 14px; animation-delay: 0.4s; }
+ }
+ }
+
+ .hint-text {
+ margin-top: 4px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ }
+ }
+ }
+
+ .mic-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ border: none;
+ background: transparent;
+ color: $primary-color;
+ cursor: pointer;
+ font-size: 12px;
+ margin-left: 5px;
+
+ &:active {
+ color: #e6454a; /* 按下变红 */
+ }
}
button {
@@ -376,11 +508,20 @@ $primary-trans-color: rgba(135, 162, 208, 0.5);
font-weight: 600;
cursor: pointer;
}
+
+ .disabled {
+ pointer-events: none;
+ opacity: 0.5;
+ }
}
}
-.disabled {
- pointer-events: none;
- opacity: 0.5;
+@keyframes wave-animation {
+ 0%, 100% {
+ transform: scaleY(1);
+ }
+ 50% {
+ transform: scaleY(1.5);
+ }
}