|
|
|
const {
|
|
|
|
mockReply
|
|
|
|
} = require('../../services/ai/mock')
|
|
|
|
|
|
|
|
const HINTS = [
|
|
|
|
'今晚还有空桌吗?',
|
|
|
|
'热销菜品有哪些?',
|
|
|
|
'今天预订情况怎么样?',
|
|
|
|
'帮我查一下订单',
|
|
|
|
]
|
|
|
|
|
|
|
|
let typingTimer = null
|
|
|
|
let recTimer = null
|
|
|
|
let recStart = 0
|
|
|
|
let touchStartY = 0
|
|
|
|
let stopped = false
|
|
|
|
let recordToken = 0
|
|
|
|
let recorderBound = false
|
|
|
|
const recorder = wx.getRecorderManager()
|
|
|
|
|
|
|
|
function bindRecorder() {
|
|
|
|
if (recorderBound) return
|
|
|
|
recorderBound = true
|
|
|
|
recorder.onStop((res) => {
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
const page = pages[pages.length - 1]
|
|
|
|
if (page && typeof page.handleRecordStop === 'function') {
|
|
|
|
page.handleRecordStop(res)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
recorder.onError((err) => {
|
|
|
|
const pages = getCurrentPages()
|
|
|
|
const page = pages[pages.length - 1]
|
|
|
|
if (page && typeof page.handleRecordError === 'function') {
|
|
|
|
page.handleRecordError(err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function pad2(n) {
|
|
|
|
return n < 10 ? `0${n}` : `${n}`
|
|
|
|
}
|
|
|
|
|
|
|
|
function timeStr(date) {
|
|
|
|
date = date || new Date()
|
|
|
|
return `${pad2(date.getHours())}:${pad2(date.getMinutes())}`
|
|
|
|
}
|
|
|
|
|
|
|
|
function uid() {
|
|
|
|
return `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
|
|
|
|
}
|
|
|
|
|
|
|
|
function clearTimers() {
|
|
|
|
if (typingTimer) {
|
|
|
|
clearInterval(typingTimer)
|
|
|
|
typingTimer = null
|
|
|
|
}
|
|
|
|
if (recTimer) {
|
|
|
|
clearInterval(recTimer)
|
|
|
|
recTimer = null
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Page({
|
|
|
|
data: {
|
|
|
|
messages: [],
|
|
|
|
inputText: '',
|
|
|
|
scrollIntoView: '',
|
|
|
|
streaming: false,
|
|
|
|
recording: false,
|
|
|
|
cancelHint: false,
|
|
|
|
recTime: '0:00',
|
|
|
|
confirmVisible: false,
|
|
|
|
confirmText: '',
|
|
|
|
recognizing: false,
|
|
|
|
showHints: true,
|
|
|
|
hints: HINTS,
|
|
|
|
storeName: '高新店',
|
|
|
|
},
|
|
|
|
|
|
|
|
onLoad() {
|
|
|
|
bindRecorder()
|
|
|
|
this._recorderOn = false
|
|
|
|
this._ignoreRecord = true
|
|
|
|
this.seedWelcome()
|
|
|
|
},
|
|
|
|
|
|
|
|
onUnload() {
|
|
|
|
clearTimers()
|
|
|
|
},
|
|
|
|
|
|
|
|
seedWelcome() {
|
|
|
|
const welcome = {
|
|
|
|
id: uid(),
|
|
|
|
role: 'agent',
|
|
|
|
text: '你好,我是经营助手。今天想问点什么?比如:"今天营业额多少?"(当前为本地演示数据)',
|
|
|
|
meta: `经营助手 · ${timeStr()}`,
|
|
|
|
}
|
|
|
|
this.setData({
|
|
|
|
messages: [welcome]
|
|
|
|
}, () => this.scrollBottom())
|
|
|
|
},
|
|
|
|
|
|
|
|
scrollBottom() {
|
|
|
|
this.setData({
|
|
|
|
scrollIntoView: ''
|
|
|
|
}, () => {
|
|
|
|
this.setData({
|
|
|
|
scrollIntoView: 'msg-bottom'
|
|
|
|
})
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
updateMessage(id, patch) {
|
|
|
|
const messages = this.data.messages.map((m) =>
|
|
|
|
m.id === id ? Object.assign({}, m, patch) : m
|
|
|
|
)
|
|
|
|
this.setData({
|
|
|
|
messages
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
onInput(e) {
|
|
|
|
this.setData({
|
|
|
|
inputText: e.detail.value
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
onHintTap(e) {
|
|
|
|
const text = e.currentTarget.dataset.text
|
|
|
|
this.setData({
|
|
|
|
inputText: text,
|
|
|
|
showHints: false
|
|
|
|
})
|
|
|
|
this.send(text)
|
|
|
|
},
|
|
|
|
|
|
|
|
onSend() {
|
|
|
|
this.send(this.data.inputText)
|
|
|
|
},
|
|
|
|
|
|
|
|
onStop() {
|
|
|
|
stopped = true
|
|
|
|
},
|
|
|
|
|
|
|
|
send(raw) {
|
|
|
|
const q = (raw || '').trim()
|
|
|
|
if (!q || this.data.streaming || this.data.recording) return
|
|
|
|
|
|
|
|
const userMsg = {
|
|
|
|
id: uid(),
|
|
|
|
role: 'user',
|
|
|
|
text: q,
|
|
|
|
meta: timeStr(),
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setData({
|
|
|
|
messages: this.data.messages.concat([userMsg]),
|
|
|
|
inputText: '',
|
|
|
|
showHints: false,
|
|
|
|
confirmVisible: false,
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
this.scrollBottom()
|
|
|
|
setTimeout(() => this.streamMock(mockReply(q)), 400)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
|
|
|
|
|
|
|
streamMock(reply) {
|
|
|
|
const id = uid()
|
|
|
|
const holder = {
|
|
|
|
id,
|
|
|
|
role: 'agent',
|
|
|
|
text: '',
|
|
|
|
meta: `经营助手 · ${timeStr()}`,
|
|
|
|
typing: true,
|
|
|
|
}
|
|
|
|
|
|
|
|
stopped = false
|
|
|
|
this.setData({
|
|
|
|
messages: this.data.messages.concat([holder]),
|
|
|
|
streaming: true,
|
|
|
|
},
|
|
|
|
() => this.scrollBottom()
|
|
|
|
)
|
|
|
|
|
|
|
|
let i = 0
|
|
|
|
const full = reply.text
|
|
|
|
if (typingTimer) clearInterval(typingTimer)
|
|
|
|
|
|
|
|
typingTimer = setInterval(() => {
|
|
|
|
if (stopped) {
|
|
|
|
clearInterval(typingTimer)
|
|
|
|
typingTimer = null
|
|
|
|
const current = this.data.messages.find((m) => m.id === id)
|
|
|
|
this.updateMessage(id, {
|
|
|
|
typing: false,
|
|
|
|
showCaret: false,
|
|
|
|
stopped: true,
|
|
|
|
text: ((current && current.text) || '').replace(/\s+$/, '') || full.slice(0, Math.max(i, 2)),
|
|
|
|
})
|
|
|
|
this.setData({
|
|
|
|
streaming: false
|
|
|
|
})
|
|
|
|
this.scrollBottom()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
i += 2
|
|
|
|
this.updateMessage(id, {
|
|
|
|
typing: false,
|
|
|
|
showCaret: i < full.length,
|
|
|
|
text: full.slice(0, i),
|
|
|
|
})
|
|
|
|
this.scrollBottom()
|
|
|
|
|
|
|
|
if (i >= full.length) {
|
|
|
|
clearInterval(typingTimer)
|
|
|
|
typingTimer = null
|
|
|
|
this.updateMessage(id, {
|
|
|
|
typing: false,
|
|
|
|
showCaret: false,
|
|
|
|
text: full,
|
|
|
|
card: reply.card,
|
|
|
|
})
|
|
|
|
this.setData({
|
|
|
|
streaming: false
|
|
|
|
})
|
|
|
|
this.scrollBottom()
|
|
|
|
}
|
|
|
|
}, 24)
|
|
|
|
},
|
|
|
|
|
|
|
|
onVoiceStart(e) {
|
|
|
|
if (this.data.streaming || this.data.recognizing) return
|
|
|
|
const touch = e.touches[0]
|
|
|
|
touchStartY = touch ? touch.clientY : 0
|
|
|
|
recStart = Date.now()
|
|
|
|
recordToken += 1
|
|
|
|
this._recordToken = recordToken
|
|
|
|
this._ignoreRecord = false
|
|
|
|
this.setData({
|
|
|
|
recording: true,
|
|
|
|
cancelHint: false,
|
|
|
|
recTime: '0:00',
|
|
|
|
confirmVisible: false,
|
|
|
|
confirmText: '',
|
|
|
|
})
|
|
|
|
|
|
|
|
if (recTimer) clearInterval(recTimer)
|
|
|
|
recTimer = setInterval(() => {
|
|
|
|
const s = Math.floor((Date.now() - recStart) / 1000)
|
|
|
|
this.setData({
|
|
|
|
recTime: `0:${pad2(s)}`
|
|
|
|
})
|
|
|
|
}, 200)
|
|
|
|
|
|
|
|
try {
|
|
|
|
recorder.start({
|
|
|
|
duration: 60000,
|
|
|
|
sampleRate: 16000,
|
|
|
|
numberOfChannels: 1,
|
|
|
|
encodeBitRate: 48000,
|
|
|
|
format: 'mp3',
|
|
|
|
})
|
|
|
|
this._recorderOn = true
|
|
|
|
} catch (err) {
|
|
|
|
this._recorderOn = false
|
|
|
|
this._ignoreRecord = true
|
|
|
|
this.setData({
|
|
|
|
recording: false
|
|
|
|
})
|
|
|
|
wx.showToast({
|
|
|
|
title: '无法开始录音',
|
|
|
|
icon: 'none'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onVoiceMove(e) {
|
|
|
|
if (!this.data.recording) return
|
|
|
|
const touch = e.touches[0]
|
|
|
|
if (!touch) return
|
|
|
|
const dy = touchStartY - touch.clientY
|
|
|
|
const cancelHint = dy > 60
|
|
|
|
if (cancelHint !== this.data.cancelHint) {
|
|
|
|
this.setData({
|
|
|
|
cancelHint
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onVoiceCancel() {
|
|
|
|
this.finishVoice(true)
|
|
|
|
},
|
|
|
|
|
|
|
|
onVoiceEnd() {
|
|
|
|
this.finishVoice(this.data.cancelHint)
|
|
|
|
},
|
|
|
|
|
|
|
|
finishVoice(cancelled) {
|
|
|
|
if (!this.data.recording) return
|
|
|
|
if (recTimer) {
|
|
|
|
clearInterval(recTimer)
|
|
|
|
recTimer = null
|
|
|
|
}
|
|
|
|
const dur = Math.round((Date.now() - recStart) / 1000)
|
|
|
|
const tooShort = !cancelled && dur < 1
|
|
|
|
this._ignoreRecord = cancelled || tooShort
|
|
|
|
this._pendingToken = this._ignoreRecord ? 0 : this._recordToken
|
|
|
|
this.setData({
|
|
|
|
recording: false,
|
|
|
|
cancelHint: false
|
|
|
|
})
|
|
|
|
|
|
|
|
if (this._recorderOn) {
|
|
|
|
this._recorderOn = false
|
|
|
|
try {
|
|
|
|
recorder.stop()
|
|
|
|
} catch (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cancelled) return
|
|
|
|
if (tooShort) {
|
|
|
|
wx.showToast({
|
|
|
|
title: '录音太短,请重说',
|
|
|
|
icon: 'none'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
handleRecordStop(res) {
|
|
|
|
if (this._ignoreRecord || !this._pendingToken || this._pendingToken !== this._recordToken) return
|
|
|
|
this._pendingToken = 0
|
|
|
|
if (!(res && res.tempFilePath)) {
|
|
|
|
wx.showToast({
|
|
|
|
title: '没有录到声音',
|
|
|
|
icon: 'none'
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// 演示阶段不接语音识别,后续接后端接口
|
|
|
|
wx.showToast({
|
|
|
|
title: '演示模式暂不支持语音',
|
|
|
|
icon: 'none'
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
handleRecordError(err) {
|
|
|
|
this._recorderOn = false
|
|
|
|
this._ignoreRecord = true
|
|
|
|
if (recTimer) {
|
|
|
|
clearInterval(recTimer)
|
|
|
|
recTimer = null
|
|
|
|
}
|
|
|
|
this.setData({
|
|
|
|
recording: false,
|
|
|
|
recognizing: false
|
|
|
|
})
|
|
|
|
const msg = (err && err.errMsg) || ''
|
|
|
|
if (/auth|deny|permission/i.test(msg)) {
|
|
|
|
wx.showModal({
|
|
|
|
title: '需要麦克风权限',
|
|
|
|
content: '请在设置中允许使用麦克风,再按住说话。',
|
|
|
|
confirmText: '去设置',
|
|
|
|
success(res) {
|
|
|
|
if (res.confirm) wx.openSetting()
|
|
|
|
},
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (this.data.recording || /start/i.test(msg)) {
|
|
|
|
wx.showToast({
|
|
|
|
title: '录音失败,请重试',
|
|
|
|
icon: 'none'
|
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onConfirmInput(e) {
|
|
|
|
this.setData({
|
|
|
|
confirmText: e.detail.value
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
onCancelConfirm() {
|
|
|
|
this.setData({
|
|
|
|
confirmVisible: false,
|
|
|
|
confirmText: ''
|
|
|
|
})
|
|
|
|
},
|
|
|
|
|
|
|
|
onOkConfirm() {
|
|
|
|
const t = this.data.confirmText.trim()
|
|
|
|
this.setData({
|
|
|
|
confirmVisible: false,
|
|
|
|
confirmText: ''
|
|
|
|
})
|
|
|
|
if (t) this.send(t)
|
|
|
|
},
|
|
|
|
}) |
|
|
\ No newline at end of file |
...
|
...
|
|