Files
lanecarford_front/src/views/asistant/components/InputArea.vue
2025-12-01 11:30:31 +08:00

414 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="input-area">
<div class="shortcut-container flex">
<div
class="shortcut-item flex flex-center"
v-for="item in shortcutList"
:key="item"
@click="handleShortcut(item)"
>
{{ item }}
</div>
</div>
<div class="input-container">
<div class="icon-wrapper">
<SvgIcon v-if="!isRecording" name="plus" size="40" />
<SvgIcon v-else name="pause" size="60" @click="stopRecording" />
</div>
<div class="divider"></div>
<div class="input-wrapper">
<textarea
id="textarea"
v-show="!isRecording"
v-model="inputValue"
rows="1"
placeholder="Ask anything about your desired outfit"
class="text-input"
@keydown="handleKeyDown"
@input="handleInput"
ref="textareaRef"
></textarea>
<div v-show="isRecording" class="recording-wrapper">
<AudioVisualizer ref="audioVisualizerRef" />
</div>
<!-- <div v-show="showAudioRecorder" class="audio-recorder-wrapper">
<AudioRecorder
ref="audioRecorderRef"
@recording-started="onRecordingStarted"
@recording-stopped="onRecordingStopped"
@recording-deleted="onRecordingDeleted"
/>
</div> -->
</div>
<div class="icon-wrapper" v-show="!isRecording" @click="handleClickAudio">
<SvgIcon name="audio" size="52" />
</div>
<!-- 音频录制图标 -->
<!-- <div class="icon-wrapper" v-show="!isRecording" @click="toggleAudioRecorder">
<SvgIcon name="download" size="52" />
</div> -->
<div class="icon-wrapper send-icon" @click="handleSend">
<SvgIcon name="send" size="46" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted, nextTick } from 'vue'
import AudioVisualizer from './AudioVisualizer.vue'
// import AudioRecorder from './AudioRecorder.vue'
import { showToast } from 'vant'
import { getAudioFileInfo, uploadAudioFile, prepareAudioForTTS } from '@/utils/audioUtils'
const emit = defineEmits(['send-message'])
const inputValue = ref<string>('')
const isRecording = ref<boolean>(false)
const showAudioRecorder = ref<boolean>(false)
const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null)
// const audioRecorderRef = ref<InstanceType<typeof AudioRecorder> | null>(null)
const textareaRef = ref<HTMLTextAreaElement | null>(null)
const shortcutList: string[] = [
'Suggest shoe styles',
'Recommend evening bags',
'Suggest accessory combinations',
'Suggest color combinations',
'Suggest fabric combinations',
'Suggest style combinations'
]
const handleSend = (): void => {
if (inputValue.value.trim()) {
// console.log('input发送消息:', inputValue.value)
emit('send-message', inputValue.value)
inputValue.value = ''
// 重置textarea高度
if (textareaRef.value) {
textareaRef.value.style.height = 'auto'
}
}
}
// 处理键盘事件
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
handleSend()
}
}
// 处理输入事件,自动调整高度
const handleInput = (): void => {
if (textareaRef.value) {
textareaRef.value.style.height = 'auto'
const scrollHeight = textareaRef.value.scrollHeight
textareaRef.value.style.height = `${scrollHeight}px`
}
}
const handleShortcut = (item: string): void => {
inputValue.value = item
}
// 语音识别相关变量
let speechRecognition = null
let lastTranscript = '' // 用于防重复
onUnmounted(() => {
if (speechRecognition) {
speechRecognition = null
}
})
const handleClickAudio = async (): Promise<void> => {
isRecording.value = !isRecording.value
// 当开始录音时等待DOM更新后触发AudioVisualizer重新计算
if (isRecording.value) {
await nextTick()
// 立即尝试更新
if (audioVisualizerRef.value) {
audioVisualizerRef.value.updateLines?.()
}
// 快速重试
setTimeout(() => {
if (audioVisualizerRef.value) {
audioVisualizerRef.value.updateLines?.()
}
}, 50)
startRecording()
} else {
stopRecording()
}
}
// 切换音频录制器显示
const toggleAudioRecorder = () => {
showAudioRecorder.value = !showAudioRecorder.value
if (showAudioRecorder.value) {
showToast('音频录制功能已启用')
}
}
// 音频录制事件处理
const onRecordingStarted = () => {
console.log('音频录制开始')
showToast('开始录制音频')
}
const onRecordingStopped = async (audioBlob: Blob) => {
console.log('音频录制结束', audioBlob)
showToast('音频录制完成')
try {
// 获取音频文件信息
const audioFile = await getAudioFileInfo(audioBlob)
console.log('音频文件信息:', audioFile)
// 为第三方TTS服务准备数据示例
const ttsData = await prepareAudioForTTS(audioBlob, 'openai')
console.log('TTS服务数据:', ttsData)
// 这里可以将音频文件发送到服务器或进行其他处理
// 例如await uploadAudioFile(audioFile, '/api/upload-audio')
showToast(`音频文件大小: ${(audioFile.size / 1024).toFixed(2)}KB`)
} catch (error) {
console.error('处理音频文件失败:', error)
showToast('处理音频文件失败')
}
}
const onRecordingDeleted = () => {
console.log('音频录制已删除')
showToast('音频录制已删除')
}
// 开始语音识别
const startRecording = () => {
if (!speechRecognition) {
// 检查浏览器支持
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
// alert('您的浏览器不支持语音识别功能')
showToast('Your browser does not support speech recognition, please try again with another browser')
isRecording.value = false
return
}
// 创建语音识别对象
const SpeechRecognition =
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
speechRecognition = new SpeechRecognition()
// 配置参数
speechRecognition.continuous = true
speechRecognition.interimResults = true
speechRecognition.lang = 'en-US' // 设置为英文
}
// 识别开始
speechRecognition.onstart = () => {
console.log('开始语音识别')
isRecording.value = true
}
// 识别结果
speechRecognition.onresult = (event: any) => {
let finalTranscript = ''
let interimTranscript = ''
// 处理所有识别结果
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript
if (event.results[i].isFinal) {
finalTranscript += transcript
} else {
interimTranscript += transcript
}
}
// 只处理最终结果,避免重复
if (finalTranscript && finalTranscript !== lastTranscript) {
console.log('最终识别结果:', finalTranscript)
lastTranscript = finalTranscript
// 将识别结果填入输入框并发送
inputValue.value = finalTranscript
// emit('send-message', finalTranscript)
}
// 显示临时结果(可选)
if (interimTranscript) {
console.log('语音转文字识别中:', interimTranscript)
}
}
// 识别结束
speechRecognition.onend = () => {
console.log('语音识别结束')
isRecording.value = false
// 清空防重复变量
lastTranscript = ''
}
// 识别错误
speechRecognition.onerror = (event: any) => {
console.error('语音识别错误:', event.error)
isRecording.value = false
// alert('语音识别失败,请重试')
showToast('Speech recognition failed, please try again')
showToast(event.error)
}
// 开始识别
speechRecognition.start()
}
// 停止语音识别
const stopRecording = () => {
if (speechRecognition && isRecording.value) {
speechRecognition.stop()
isRecording.value = false
}
}
</script>
<style lang="less" scoped>
.input-area {
background-color: #fff;
}
.shortcut-container {
overflow-x: auto;
flex-wrap: nowrap;
padding: 0 4.4rem;
column-gap: 1.2rem;
margin-bottom: 2.84rem;
&::-webkit-scrollbar {
display: none;
}
.shortcut-item {
font-size: 4.2rem;
width: fit-content;
font-family: 'robotoRegular';
white-space: nowrap;
height: 8.1rem;
line-height: 8.1rem;
padding: 2.2rem 2.8rem;
color: #6d6868;
background-color: #efefef;
border-radius: 2rem;
}
}
.input-container {
display: flex;
align-items: center;
background-color: #efefef;
padding: 1.5rem 4.85rem 1.5rem 5.2rem;
min-height: 19.3rem;
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #6d6868;
&.send-icon {
margin-left: 4.38rem;
}
}
.divider {
width: 2px;
height: 14.9rem;
margin-left: 5.59rem;
margin-right: 3.5rem;
background-color: #888;
align-self: center;
}
.input-wrapper {
flex: 1;
display: flex;
align-items: center;
overflow: hidden;
}
.text-input {
width: 100%;
// min-height: 4.8rem;
max-height: 14.4rem;
border: none;
outline: none;
background: transparent;
font-size: 4rem;
font-family: 'robotoRegular';
font-weight: 400;
line-height: 4.8rem; /* 设置行高等于实际渲染高度,实现垂直居中 */
padding: 0;
color: #000;
word-break: break-all;
&::placeholder {
color: #888;
letter-spacing: -0.01em;
font-weight: 400;
font-family: 'robotoRegular';
word-spacing: -5px;
line-height: 4.8rem;
}
}
// 确保图标颜色为 #6d6868
:deep(.svg-icon) {
color: #6d6868;
svg {
fill: #6d6868;
}
}
// 录制状态样式
.recording-wrapper {
flex: 1;
display: flex;
align-items: center;
min-height: 4.8rem;
}
// 音频录制器样式
.audio-recorder-wrapper {
flex: 1;
display: flex;
align-items: center;
min-height: 4.8rem;
padding: 10px 0;
}
.recording-animation {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1.5rem;
}
.recording-text {
font-family: 'robotoRegular';
font-size: 2.8rem;
color: #6d6868;
letter-spacing: 0.02em;
}
</style>