This commit is contained in:
Felix
2026-01-26 17:52:22 +08:00
parent 1bffa69aea
commit 96b0a20fa0
5 changed files with 99 additions and 11 deletions

View File

@@ -134,6 +134,7 @@ interface IData {
showChatSuggestions: boolean
chatSuggestions: Array<{ label: string; text: string }>
inputBottom: number
isRecording: boolean
}
interface IPageInstance {
@@ -144,6 +145,10 @@ interface IPageInstance {
suggestionTimer?: number
isSuggestionTouching?: boolean
audioCtx?: WechatMiniprogram.InnerAudioContext
recorderManager?: WechatMiniprogram.RecorderManager
handleRecordStart: () => void
handleRecordEnd: () => void
uploadAndRecognizeAudio: (filePath: string) => Promise<void>
onKeyboardHeightChange: (res: any) => void
startPlaceholderTimer: () => void
startSuggestionTimer: () => void
@@ -342,13 +347,63 @@ Page<IData, IPageInstance>({
chatPlaceholder: '请输入...',
showChatSuggestions: false,
chatSuggestions: [],
inputBottom: 0
inputBottom: 0,
isRecording: false
},
onKeyboardHeightChange(res: any) {
this.setData({ inputBottom: res.height })
},
handleRecordStart() {
if (!this.recorderManager) return
wx.authorize({
scope: 'scope.record',
success: () => {
this.recorderManager?.start({ format: 'mp3' })
},
fail: () => {
wx.showModal({
title: '提示',
content: '需要录音权限才能使用语音输入',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
})
},
handleRecordEnd() {
if (this.data.isRecording && this.recorderManager) {
this.recorderManager.stop()
}
},
async uploadAndRecognizeAudio(filePath: string) {
wx.showLoading({ title: '识别中...' })
try {
const fileId = await apiManager.uploadFile(filePath)
const sessionId = this.data.conversationLatestSession?.session_id
if (!sessionId) throw new Error('没有会话ID')
const res = await apiManager.recognizeQaAudio(sessionId, fileId)
if (res.data && res.data.text) {
this.setData({ chatInputValue: res.data.text })
} else {
wx.showToast({ title: '未能识别出文字', icon: 'none' })
}
} catch (e) {
console.error('语音识别失败', e)
wx.showToast({ title: '识别失败', icon: 'none' })
} finally {
wx.hideLoading()
}
},
pollTimer: undefined,
variationPollTimer: undefined,
conversationPollTimer: undefined,
@@ -560,6 +615,26 @@ Page<IData, IPageInstance>({
async onLoad(options: Record<string, string>) {
wx.onKeyboardHeightChange(this.onKeyboardHeightChange)
this.recorderManager = wx.getRecorderManager()
this.recorderManager.onStart(() => {
this.setData({ isRecording: true })
wx.showToast({ title: '正在录音...', icon: 'none', duration: 60000 })
})
this.recorderManager.onStop((res) => {
this.setData({ isRecording: false })
wx.hideToast()
const { tempFilePath, duration } = res
if (duration < 500) {
wx.showToast({ title: '说话时间太短', icon: 'none' })
return
}
this.uploadAndRecognizeAudio(tempFilePath)
})
this.recorderManager.onError((err) => {
console.error('录音报错', err)
this.setData({ isRecording: false })
wx.showToast({ title: '录音失败', icon: 'none' })
})
try {
const app = getApp<IAppOption>()

View File

@@ -25,12 +25,11 @@
</block>
</view>
<view class="question-scroll-wrapper {{questionMode === 'conversation' && conversationViewMode === 'chat' && isChatInputVisible ? 'chat-input-mode-scroll' : 'chat-mode-scroll'}}">
<scroll-view class="inner-scroll" scroll-y="true" scroll-into-view="{{scrollIntoView}}" scroll-with-animation>
<scroll-view class="inner-scroll" scroll-y >
<view class="image-card" wx:if="{{questionMode !== 'variation'}}">
<image wx:if="{{imageLocalUrl}}" class="image" src="{{imageLocalUrl}}" mode="aspectFill" bindtap="previewImage" bindload="onImageLoad" binderror="onImageError"></image>
<view class="view-full" wx:if="{{imageLocalUrl}}" bindtap="previewImage">
<t-icon name="zoom-in" size="32rpx" />
<!-- <text>View Full</text> -->
</view>
</view>
<view class="question-title" wx:if="{{questionMode !== 'conversation'}}">
@@ -48,7 +47,6 @@
</view>
</view>
<view class="question-content {{modeAnim}}" wx:if="{{questionMode === 'cloze'}}">
<!-- <view class="choice-title">Select the correct word to complete the sentence:</view> -->
<view class="cloze-sentence">
<text wx:for="{{clozeSentenceTokens}}" wx:key="index" class="{{item.isBlank ? 'cloze-fill' : 'cloze-text'}}" data-word="{{item.word}}" bindtap="handleWordClick">{{item.text}}</text>
</view>
@@ -338,9 +336,12 @@
bind:blur="onChatBlur"
>
<view slot="footer-prefix" class="footer-prefix">
<view class="chat-icon-block" bind:tap="onChatCloseTap">
<view class="chat-icon-block" bind:tap="onChatCloseTap" style="margin-right: 16rpx;">
<t-icon name="close-circle" size="64rpx" color="#dcdcdc"/>
</view>
<view class="chat-icon-block" bind:touchstart="handleRecordStart" bind:touchend="handleRecordEnd">
<t-icon name="microphone-1" size="64rpx" color="{{isRecording ? '#0052d9' : '#dcdcdc'}}"/>
</view>
</view>
</t-chat-sender>
</view>
@@ -383,7 +384,7 @@
<text class="modal-title">题目解析</text>
<t-icon name="close" class="modal-close" size="40rpx" bind:tap="onCloseDetailModal" />
</view>
<scroll-view class="detail-body" scroll-y="true">
<scroll-view class="detail-body" scroll-y="{{true}}">
<view class="section">
<text class="question-text">{{qaDetailQuestionText}}</text>
</view>

View File

@@ -26,17 +26,20 @@
.question-scroll-wrapper {
flex: 1;
height: 0;
min-height: 0;
display: block;
width: 100%;
position: relative;
overflow: hidden;
/* padding-bottom: calc(110rpx + env(safe-area-inset-bottom)); */
/* transition: padding-bottom 0.3s ease; */
transition: margin-bottom 0.3s ease;
}
.inner-scroll {
height: 100%;
position: absolute;
top: 0;
left: 0;
width: 100%;
/* height: 100%; */
height: 500rpx;
}
.chat-mode-scroll {
/* padding-bottom: calc(110rpx + env(safe-area-inset-bottom)); */

View File

@@ -18,7 +18,7 @@
<text class="sentence-zh">{{scene.list[currentIndex].sentenceZh}}</text>
</view>
<scroll-view class="sentence-body" scroll-y="true" scroll-with-animation="true">
<scroll-view class="sentence-body" scroll-y="{{true}}" scroll-with-animation="true">
<view class="tags-wrap">
<t-tag wx:for="{{scene.list[currentIndex].functionTags}}" wx:key="idx" variant="light"><text>#</text>{{item}}</t-tag>
</view>

View File

@@ -687,6 +687,15 @@ class ApiManager {
})
}
// 语音识别
async recognizeQaAudio(sessionId: string, fileId: string): Promise<IApiResponse<any>> {
return this.request(
`/api/v1/qa/conversations/${sessionId}/recognize_audio`,
'POST',
{ file_id: fileId }
)
}
// 上传文件第一步上传文件获取ID
async uploadFile(filePath: string, retryCount: number = 0): Promise<string> {
if (USE_CLOUD) {