|
|
<script setup lang="ts"> |
|
|
|
|
|
import router from "@/router.ts"; |
|
|
import { useSettingsStore } from "@/stores/config.ts"; |
|
|
import { onMounted, onUnmounted, ref, reactive } from "vue"; |
|
|
import { SettingTwoTone } from "@ant-design/icons-vue"; |
|
|
import DynamicBall from "@/views/Home/Components/DynamicBall.vue"; |
|
|
import ChatText from "@/views/Home/Components/ChatText.vue"; |
|
|
import axios from "axios"; |
|
|
import { test_server } from "@/config/client_config.ts"; |
|
|
|
|
|
const base_url = axios.defaults.baseURL |
|
|
|
|
|
const settingsStore = useSettingsStore() |
|
|
|
|
|
import mic_on from "@/assets/microphone.png" |
|
|
import mic_off from "@/assets/microphone_off.png" |
|
|
import text_on from "@/assets/text.png" |
|
|
import text_off from "@/assets/text_off.png" |
|
|
import close from "@/assets/close.png" |
|
|
import download from "@/assets/download.png" |
|
|
|
|
|
const host = import.meta.env.PROD ? window.location.host : test_server |
|
|
|
|
|
let ws_prefix = 'ws' |
|
|
if (host.startsWith('127.0.0.1') || host.startsWith('localhost')) { |
|
|
ws_prefix = 'ws' |
|
|
} else { |
|
|
ws_prefix = 'wss' |
|
|
} |
|
|
const ws_url = `${ws_prefix}://` + host + `/api/v1/ws` |
|
|
console.warn('ws_url: ', ws_url) |
|
|
|
|
|
const sock = ref(null) |
|
|
const startWebSock = async () => { |
|
|
console.warn('start websocket ...') |
|
|
|
|
|
|
|
|
if (sock.value && sock.value.readyState !== WebSocket.CLOSED) { |
|
|
|
|
|
sock.value.close(); |
|
|
} |
|
|
|
|
|
|
|
|
const socket_url = `${ws_url}` |
|
|
|
|
|
sock.value = new WebSocket(socket_url) |
|
|
|
|
|
sock.value.binaryType = "arraybuffer"; |
|
|
console.warn('created web socket ...') |
|
|
|
|
|
sock.value.addEventListener('open', () => { |
|
|
console.log('WebSocket 连接成功'); |
|
|
}); |
|
|
|
|
|
sock.value.addEventListener('close', () => { |
|
|
console.log('WebSocket 连接已关闭'); |
|
|
}); |
|
|
|
|
|
sock.value.onclose = (event: any) => { |
|
|
console.log('code:', event.code, 'reason:', event.reason, 'wasClean:', event.wasClean) |
|
|
|
|
|
console.log('WebSocket 连接已关闭:', event); |
|
|
}; |
|
|
|
|
|
sock.value.addEventListener('error', (error) => { |
|
|
console.error('WebSocket 连接错误:', error); |
|
|
}); |
|
|
|
|
|
sock.value.addEventListener('message', (event) => { |
|
|
try { |
|
|
const data = JSON.parse(event.data) |
|
|
console.log('WebSocket 收到消息:', data); |
|
|
if (data) { |
|
|
updateViewData(data) |
|
|
} |
|
|
} catch (e) { |
|
|
console.error("解析 WebSocket 消息失败:", e, "原始数据:", event.data); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const stopWebSock = async () => { |
|
|
if (sock.value) { |
|
|
console.log("主动关闭 WebSocket 连接"); |
|
|
|
|
|
sock.value.close(1000, "User closed connection"); |
|
|
sock.value = null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const currentSession: any = reactive([]); |
|
|
|
|
|
const updateViewData = (data: any) => { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (data) { |
|
|
if (data['message_type'] === 'question') { |
|
|
|
|
|
if (data.question && data.question.length > 0) { |
|
|
|
|
|
|
|
|
if (currentSession.length > 0 && |
|
|
currentSession[currentSession.length - 1].type === 'question' && |
|
|
currentSession[currentSession.length - 1].task_id === data.task_id) { |
|
|
currentSession[currentSession.length - 1].content = data.question; |
|
|
} else { |
|
|
|
|
|
currentSession.push({ |
|
|
type: 'question', |
|
|
content: data.question, |
|
|
task_id: data.task_id |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
} else if (data['message_type'] === 'answer') { |
|
|
if (data.answer_index === 0) { |
|
|
currentSession.push({ |
|
|
type: 'answer', |
|
|
content: data.answer, |
|
|
task_id: data.task_id, |
|
|
}); |
|
|
} else { |
|
|
|
|
|
const lastAnswer = currentSession[currentSession.length - 1]; |
|
|
if (lastAnswer && |
|
|
lastAnswer.task_id === data.task_id && |
|
|
lastAnswer.content !== null && lastAnswer.content.length > 0) { |
|
|
if (lastAnswer.type === 'answer') { |
|
|
lastAnswer.content += data.answer; |
|
|
} else { |
|
|
console.warn('Received answer without a matching question in the current session'); |
|
|
currentSession.push({ |
|
|
type: data.type, |
|
|
content: data.answer, |
|
|
task_id: data.task_id, |
|
|
}); |
|
|
} |
|
|
|
|
|
} else { |
|
|
console.warn('Received answer without a matching question in the current session'); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
onMounted( async () => { |
|
|
|
|
|
await startWebSock(); |
|
|
}) |
|
|
|
|
|
onUnmounted( async () => { |
|
|
console.warn('onUnmounted, stop web socket ...') |
|
|
stopWebSock(); |
|
|
}); |
|
|
|
|
|
const backAction = async () => { |
|
|
const state = await stopAudioCapture(); |
|
|
if (!state) { |
|
|
console.error('Failed to stop audio chat system service'); |
|
|
return; |
|
|
} |
|
|
router.replace('/') |
|
|
} |
|
|
|
|
|
const stopActionLoading = ref<boolean>(false); |
|
|
const stopAudioCapture = async () => { |
|
|
try { |
|
|
stopActionLoading.value = true; |
|
|
|
|
|
const response = await fetch(`${base_url}/system/stop`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: null |
|
|
}); |
|
|
if (!response.ok) { |
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
const data = await response.json(); |
|
|
console.log('ASR Instance stopped successfully:', data); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Error stop record :', error); |
|
|
return false; |
|
|
} finally { |
|
|
stopActionLoading.value = false; |
|
|
} |
|
|
} |
|
|
|
|
|
const micActionLoading = ref<boolean>(false); |
|
|
const pauseAudioCapture = async () => { |
|
|
try { |
|
|
micActionLoading.value = true; |
|
|
const response = await fetch(`${base_url}/system/pause`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: null |
|
|
}); |
|
|
if (!response.ok) { |
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
const data = await response.json(); |
|
|
console.log('Mic paused successfully:', data); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Error pause mic:', error); |
|
|
return false; |
|
|
} finally { |
|
|
micActionLoading.value = false; |
|
|
} |
|
|
} |
|
|
|
|
|
const resumeAudioCapture = async () => { |
|
|
try { |
|
|
micActionLoading.value = true; |
|
|
const response = await fetch(`${base_url}/system/resume`, { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json', |
|
|
}, |
|
|
body: null |
|
|
}); |
|
|
if (!response.ok) { |
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`); |
|
|
} |
|
|
const data = await response.json(); |
|
|
console.log('Mic resume successfully:', data); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Error resume mic:', error); |
|
|
return false; |
|
|
} finally { |
|
|
micActionLoading.value = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const mic_working = ref<boolean>(true); |
|
|
const toggleMic = async () => { |
|
|
if (mic_working.value) { |
|
|
const state = await pauseAudioCapture(); |
|
|
if (!state) { |
|
|
console.error('Failed to stop audio chat system service'); |
|
|
return; |
|
|
} |
|
|
} else { |
|
|
const state = await resumeAudioCapture(); |
|
|
if (!state) { |
|
|
console.error('Failed to start audio chat system service'); |
|
|
return; |
|
|
} |
|
|
} |
|
|
mic_working.value = !mic_working.value; |
|
|
console.log('mic_state', mic_working.value); |
|
|
}; |
|
|
const text_state = ref<boolean>(false); |
|
|
const toggleText = () => { |
|
|
text_state.value = !text_state.value; |
|
|
console.log('text_state', text_state.value); |
|
|
}; |
|
|
|
|
|
|
|
|
</script> |
|
|
|
|
|
<template> |
|
|
<div class="chat-wrapper"> |
|
|
<div class="content"> |
|
|
<div v-if="!text_state"> |
|
|
<DynamicBall :isPlaying="mic_working" /> |
|
|
</div> |
|
|
<div v-if="text_state"> |
|
|
<ChatText :chatContent="currentSession" :isPlaying="mic_working" /> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="actions"> |
|
|
<div class="holder"> |
|
|
<span> </span> |
|
|
</div> |
|
|
<div class="btns"> |
|
|
<a-button type="text" style="width:64px; height: 64px;" :loading="micActionLoading" @click="toggleMic"> |
|
|
<template #icon> |
|
|
<img :src="mic_working == true ? mic_on : mic_off" width="50" height="50" alt="mic_on" /> |
|
|
</template> |
|
|
</a-button> |
|
|
<a-button type="text" style="width:64px; height: 64px;" @click="toggleText"> |
|
|
<template #icon> |
|
|
<img :src="text_state == true ? text_on : text_off" width="50" height="50" alt="text_off" /> |
|
|
</template> |
|
|
</a-button> |
|
|
<a-button type="text" style="width:64px; height: 64px;" :loading="stopActionLoading" @click="backAction"> |
|
|
<template #icon> |
|
|
<img :src="close" width="50" height="50" alt="close" /> |
|
|
</template> |
|
|
</a-button> |
|
|
</div> |
|
|
<div class="download-wrapper"> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</template> |
|
|
|
|
|
<style lang="scss" scoped> |
|
|
.chat-wrapper { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
background-image: url('@/assets/bg.png'); |
|
|
background-repeat: no-repeat; |
|
|
background-attachment: fixed; |
|
|
background-size: cover; |
|
|
background-position: center; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
color: #fff; |
|
|
|
|
|
.content { |
|
|
width: 100%; |
|
|
height: auto; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: space-around; |
|
|
|
|
|
.inner-content { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
text-align: center; |
|
|
padding: 20px; |
|
|
|
|
|
.text-box { |
|
|
color: #000; |
|
|
margin-bottom: 36px; |
|
|
|
|
|
.title { |
|
|
font-size: 24px; |
|
|
font-weight: 600; |
|
|
margin-bottom: 24px; |
|
|
} |
|
|
|
|
|
.sub-title { |
|
|
font-size: 15px; |
|
|
margin-top: 10px; |
|
|
} |
|
|
} |
|
|
|
|
|
.btn-box { |
|
|
width: 224px; |
|
|
height: 80px; |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
.actions { |
|
|
width: 100%; |
|
|
height: 100px; |
|
|
|
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: center; |
|
|
|
|
|
.holder { |
|
|
width: 64px; |
|
|
height: 48px; |
|
|
} |
|
|
.btns { |
|
|
width: 450px; |
|
|
height: 96px; |
|
|
display: flex; |
|
|
justify-content: space-around; |
|
|
align-items: flex-start; |
|
|
} |
|
|
.download-wrapper { |
|
|
width: 64px; |
|
|
height: 64px; |
|
|
display: flex; |
|
|
justify-content: flex-start; |
|
|
align-items: center; |
|
|
margin-right: 0; |
|
|
|
|
|
img { |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
</style> |
|
|
|