Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
@touchstart="onTouchStart"
@touchmove="onTouchMove"
@touchend="onTouchEnd"
:disabled="props.disabled"
:disabled="disabled"
>
按住说话
{{ disabled ? '对话中' : '按住说话' }}
</el-button>
<!-- 使用 custom-class 自定义样式 -->
<transition name="el-fade-in-linear">
Expand Down Expand Up @@ -94,10 +94,13 @@ watch(
)

function onTouchStart(event: any) {
emit('TouchStart')
startY.value = event.touches[0].clientY
// 阻止默认滚动行为
event.preventDefault()
if (props.disabled) {
return
}
emit('TouchStart')
startY.value = event.touches[0].clientY
}
function onTouchMove(event: any) {
if (!isTouching.value) return
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provided code snippet needs several corrections to ensure it compiles and behaves correctly according to expected functionality:

  1. Line 6: The colon : should be removed from :disabled="disabled".
  2. Variable Initialization: Ensure that startY is properly initialized and accessible within the component context.
  3. Watch Event Handling: Add logic to handle changes in the disabled prop.

Here's the revised code with these improvements:

<template>
  <el-button
    @touchstart="onTouchStart($event)"
    @touchmove="onTouchMove($event)"
    @touchend="onTouchEnd($event)"
    :disabled="disabled"
  >
    {{ disabled ? '对话中' : '按住说话' }}
  </el-button>

  <transition name="el-fade-in-linear">
    <!-- ... -->
  </transition>

  <watch>
    { handler(val) => (this.youTubeComponentDisabled = val) },
    immediate: true
  ></watch>
</template>

<script lang="ts">
import { Vue } from "vue";

export default class ComponentName extends Vue {
  startY: number | null = null;
  youTubeComponentDisabled: boolean = false;

  get props(): any {
    return this.$attrs;
  }

  mounted() {
    const YouTube = document.querySelector("#youtube");
    YouTube.addEventListener("click", () => (this.youTubeComponentDisabled = !this.youTubeComponentDisabled));
  }

  onTouchStart(event: TouchEvent) {
    if (this.disabled || this.startY !== null) return; // Remove unnecessary checks
    event.preventDefault();
    this.emit('TouchStart')
    this.startY = event.touches[0].clientY
  }

  onTouchMove(event: TouchEvent) {
    if (!this.isTouching.value) return;
    // ...
  }

  // Add method to emit events as per need
}
</script>

Key Changes:

  • Corrected the use of props. in the template binding (:disabled="props.disabled").
  • Initialized and used startY properly.
  • Added a watch property for handling changes to the disabled prop (although not explicitly shown in your original post, assuming it was intended).
  • Ensured consistent use of this.
  • Removed some redundant checks within onTouchStart.

These modifications will help rectify the syntax errors and potential issues in the code while maintaining its functionality.

Expand Down
229 changes: 138 additions & 91 deletions ui/src/components/ai-chat/component/chat-input-operate/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,17 @@
@TouchStart="startRecording"
@TouchEnd="TouchEnd"
:time="recorderTime"
:start="!mediaRecorderStatus"
:start="recorderStatus === 'START'"
:disabled="loading"
/>
<el-input
v-else
ref="quickInputRef"
v-model="inputValue"
:placeholder="
startRecorderTime
recorderStatus === 'START'
? `${$t('chat.inputPlaceholder.speaking')}...`
: recorderLoading
: recorderStatus === 'TRANSCRIBING'
? `${$t('chat.inputPlaceholder.recorderLoading')}...`
: $t('chat.inputPlaceholder.default')
"
Expand All @@ -143,8 +143,10 @@
<template v-if="props.applicationDetails.stt_model_enable">
<span v-if="mode === 'mobile'">
<el-button text @click="isMicrophone = !isMicrophone">
<!-- 键盘 -->
<AppIcon v-if="isMicrophone" iconName="app-keyboard"></AppIcon>
<el-icon v-else>
<!-- 录音 -->
<Microphone />
</el-icon>
</el-button>
Expand All @@ -154,7 +156,7 @@
:disabled="loading"
text
@click="startRecording"
v-if="mediaRecorderStatus"
v-if="recorderStatus === 'STOP'"
>
<el-icon>
<Microphone />
Expand All @@ -165,14 +167,19 @@
<el-text type="info"
>00:{{ recorderTime < 10 ? `0${recorderTime}` : recorderTime }}</el-text
>
<el-button text type="primary" @click="stopRecording" :loading="recorderLoading">
<el-button
text
type="primary"
@click="stopRecording"
:loading="recorderStatus === 'TRANSCRIBING'"
>
<AppIcon iconName="app-video-stop"></AppIcon>
</el-button>
</div>
</span>
</template>

<template v-if="(!startRecorderTime && !recorderLoading) || mode === 'mobile'">
<template v-if="recorderStatus === 'STOP' || mode === 'mobile'">
<span v-if="props.applicationDetails.file_upload_enable" class="flex align-center ml-4">
<el-upload
action="#"
Expand Down Expand Up @@ -234,7 +241,7 @@
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import { ref, computed, onMounted, nextTick, watch } from 'vue'
import Recorder from 'recorder-core'
import TouchChat from './TouchChat.vue'
import applicationApi from '@/api/application'
Expand Down Expand Up @@ -417,124 +424,147 @@ const uploadFile = async (file: any, fileList: any) => {
}
})
}

// 语音录制任务id
const intervalId = ref<any | null>(null)
// 语音录制开始秒数
const recorderTime = ref(0)
const startRecorderTime = ref(false)
const recorderLoading = ref(false)
// START:开始录音 TRANSCRIBING:转换文字中
const recorderStatus = ref<'START' | 'TRANSCRIBING' | 'STOP'>('STOP')

const inputValue = ref<string>('')
const uploadImageList = ref<Array<any>>([])
const uploadDocumentList = ref<Array<any>>([])
const uploadVideoList = ref<Array<any>>([])
const uploadAudioList = ref<Array<any>>([])
const mediaRecorderStatus = ref(true)

const showDelete = ref('')

// 定义响应式引用
const mediaRecorder = ref<any>(null)
const isDisabledChat = computed(
() => !(inputValue.value.trim() && (props.appId || props.applicationDetails?.name))
)
// 移动端语音
// 是否显示移动端语音按钮
const isMicrophone = ref(false)

watch(isMicrophone, (value: boolean) => {
if (value) {
// 如果显示就申请麦克风权限
recorderManage.open()
} else {
// 关闭麦克风
recorderManage.close()
}
})
const TouchEnd = (bool: Boolean) => {
if (bool) {
stopRecording()
recorderStatus.value = 'STOP'
} else {
stopTimer()
mediaRecorder.value.close()
mediaRecorder.value = null
recorderStatus.value = 'STOP'
}
}

// 开始录音
const startRecording = async () => {
try {
// 取消录音控制台日志
Recorder.CLog = function () {}
mediaRecorder.value = new Recorder({
// 取消录音控制台日志
Recorder.CLog = function () {}

class RecorderManage {
recorder?: any
uploadRecording: (blob: Blob, duration: number) => void
constructor(uploadRecording: (blob: Blob, duration: number) => void) {
this.uploadRecording = uploadRecording
}
open() {
const recorder = new Recorder({
type: 'mp3',
bitRate: 128,
sampleRate: 16000
})

mediaRecorder.value.open(
() => {
mediaRecorder.value.start()
mediaRecorderStatus.value = false
if (!this.recorder) {
recorder.open(() => {
this.recorder = recorder
}, this.errorCallBack)
}
}
start() {
if (this.recorder) {
this.recorder.start()
recorderStatus.value = 'START'
handleTimeChange()
} else {
const recorder = new Recorder({
type: 'mp3',
bitRate: 128,
sampleRate: 16000
})
recorder.open(() => {
this.recorder = recorder
recorder.start()
recorderStatus.value = 'START'
handleTimeChange()
},
(err: any) => {
stopTimer()
mediaRecorder.value.close()
MsgAlert(
t('common.tip'),
`${t('chat.tip.recorderTip')}
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
{
}, this.errorCallBack)
}
}
stop() {
if (this.recorder) {
this.recorder.stop(
(blob: Blob, duration: number) => {
if (mode !== 'mobile') {
this.close()
}
this.uploadRecording(blob, duration)
},
(err: any) => {
MsgAlert(t('common.tip'), err, {
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
}
)
}
)
} catch (error) {
MsgAlert(
t('common.tip'),
`${t('chat.tip.recorderTip')}
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
{
})
}
)
}
}
close() {
if (this.recorder) {
this.recorder.close()
this.recorder = undefined
}
}

private errorCallBack(err: any, isUserNotAllow: boolean) {
if (isUserNotAllow) {
MsgAlert(t('common.tip'), err, {
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
}
)
mediaRecorder.value.close()
stopTimer()
}
}

// 停止录音
const stopRecording = () => {
startRecorderTime.value = false
recorderTime.value = 0
if (mediaRecorder.value) {
mediaRecorderStatus.value = true
mediaRecorder.value.stop(
(blob: Blob, duration: number) => {
// 测试blob是否能正常播放
// const link = document.createElement('a')
// link.href = window.URL.createObjectURL(blob)
// link.download = 'abc.mp3'
// link.click()
uploadRecording(blob) // 上传录音文件
},
(err: any) => {
console.error(`${t('chat.tip.recorderError')}:`, err)
}
)
})
} else {
MsgAlert(
t('common.tip'),
`${err}
<div style="width: 100%;height:1px;border-top:1px var(--el-border-color) var(--el-border-style);margin:10px 0;"></div>
${t('chat.tip.recorderTip')}
<img src="${new URL(`@/assets/tipIMG.jpg`, import.meta.url).href}" style="width: 100%;" />`,
{
confirmButtonText: t('chat.tip.confirm'),
dangerouslyUseHTMLString: true,
customClass: 'record-tip-confirm'
}
)
}
}
}

// 上传录音文件
const uploadRecording = async (audioBlob: Blob) => {
try {
// 非自动发送切换输入框
if (!props.applicationDetails.stt_autosend) {
isMicrophone.value = false
}
recorderLoading.value = true

recorderStatus.value = 'TRANSCRIBING'
const formData = new FormData()
formData.append('file', audioBlob, 'recording.mp3')
bus.emit('on:transcribing', true)
applicationApi
.postSpeechToText(props.applicationDetails.id as string, formData, localLoading)
.then((response) => {
recorderLoading.value = false
mediaRecorder.value.close()
inputValue.value = typeof response.data === 'string' ? response.data : ''
// 自动发送
if (props.applicationDetails.stt_autosend) {
Expand All @@ -546,21 +576,35 @@ const uploadRecording = async (audioBlob: Blob) => {
}
})
.catch((error) => {
recorderLoading.value = false
console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
})
.finally(() => bus.emit('on:transcribing', false))
.finally(() => {
recorderStatus.value = 'STOP'
bus.emit('on:transcribing', false)
})
} catch (error) {
recorderLoading.value = false
recorderStatus.value = 'STOP'
console.error(`${t('chat.uploadFile.errorMessage')}:`, error)
}
}
const recorderManage = new RecorderManage(uploadRecording)
// 开始录音
const startRecording = () => {
recorderManage.start()
}

// 停止录音
const stopRecording = () => {
recorderManage.stop()
}

const handleTimeChange = () => {
startRecorderTime.value = true
recorderTime.value = 0
if (intervalId.value) {
return
}
intervalId.value = setInterval(() => {
if (!startRecorderTime.value) {
if (recorderStatus.value === 'STOP') {
clearInterval(intervalId.value!)
intervalId.value = null
return
Expand All @@ -569,20 +613,21 @@ const handleTimeChange = () => {
recorderTime.value++

if (recorderTime.value === 60) {
stopRecording()
clearInterval(intervalId.value!)
intervalId.value = null
startRecorderTime.value = false
if (mode !== 'mobile') {
stopRecording()
clearInterval(intervalId.value!)
intervalId.value = null
recorderStatus.value = 'STOP'
}
}
}, 1000)
}
// 停止计时的函数
const stopTimer = () => {
if (intervalId.value !== null) {
clearInterval(intervalId.value)
recorderTime.value = 0
intervalId.value = null
startRecorderTime.value = false
mediaRecorderStatus.value = true
}
}

Expand All @@ -598,7 +643,9 @@ function autoSendMessage() {
uploadDocumentList.value = []
uploadAudioList.value = []
uploadVideoList.value = []
quickInputRef.value.textareaStyle.height = '45px'
if (quickInputRef.value) {
quickInputRef.value.textareaStyle.height = '45px'
}
}

function sendChatHandle(event?: any) {
Expand Down
Loading