Xin Zhang
commited on
Commit
·
7bff52e
1
Parent(s):
8587958
frontend
Browse files- frontend/.gitattributes +39 -0
- frontend/.gitignore +29 -0
- frontend/README.md +20 -0
- frontend/index.html +13 -0
- frontend/package.json +3 -0
- frontend/postcss.config.js +3 -0
- frontend/public/favicon.ico +3 -0
- frontend/src/App.vue +100 -0
- frontend/src/assets/ball.json +3 -0
- frontend/src/assets/bg.png +3 -0
- frontend/src/assets/close.png +3 -0
- frontend/src/assets/download.png +3 -0
- frontend/src/assets/icon.png +3 -0
- frontend/src/assets/microphone.png +3 -0
- frontend/src/assets/microphone_off.png +3 -0
- frontend/src/assets/setting.png +3 -0
- frontend/src/assets/text.png +3 -0
- frontend/src/assets/text_off.png +3 -0
- frontend/src/config/axios/config.ts +48 -0
- frontend/src/config/axios/index.ts +75 -0
- frontend/src/config/client_config.ts +23 -0
- frontend/src/hooks/showError.ts +3 -0
- frontend/src/hooks/useCache.ts +17 -0
- frontend/src/index.d.ts +34 -0
- frontend/src/main.ts +31 -0
- frontend/src/router.ts +49 -0
- frontend/src/stores/config.ts +18 -0
- frontend/src/stores/measure.ts +15 -0
- frontend/src/stores/session.ts +269 -0
- frontend/src/style.scss +175 -0
- frontend/src/utils/audio_utils.ts +83 -0
- frontend/src/utils/retry.ts +27 -0
- frontend/src/utils/size.ts +47 -0
- frontend/src/views/404/index.vue +24 -0
- frontend/src/views/Footer.vue +23 -0
- frontend/src/views/Header.vue +200 -0
- frontend/src/views/Home/Components/ChatText.vue +119 -0
- frontend/src/views/Home/Components/DynamicBall.vue +74 -0
- frontend/src/views/Home/index.vue +421 -0
- frontend/src/views/Home/index2.vue +934 -0
- frontend/src/views/Settings/index.vue +103 -0
- frontend/src/views/Welcome/index.vue +317 -0
- frontend/src/vite-env.d.ts +1 -0
- frontend/tailwind.config.js +3 -0
- frontend/tsconfig.json +3 -0
- frontend/tsconfig.node.json +3 -0
- frontend/vite.config.ts +53 -0
frontend/.gitattributes
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
*.icns filter=lfs diff=lfs merge=lfs -text
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
www
|
| 14 |
+
www-ssr
|
| 15 |
+
*.local
|
| 16 |
+
|
| 17 |
+
.env
|
| 18 |
+
.cursor
|
| 19 |
+
|
| 20 |
+
# Editor directories and files
|
| 21 |
+
.vscode
|
| 22 |
+
!.vscode/extensions.json
|
| 23 |
+
.idea
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.suo
|
| 26 |
+
*.ntvs*
|
| 27 |
+
*.njsproj
|
| 28 |
+
*.sln
|
| 29 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
| 6 |
+
|
| 7 |
+
## Recommended IDE Setup
|
| 8 |
+
|
| 9 |
+
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
| 10 |
+
|
| 11 |
+
## Type Support For `.vue` Imports in TS
|
| 12 |
+
|
| 13 |
+
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
| 14 |
+
|
| 15 |
+
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
| 16 |
+
|
| 17 |
+
1. Disable the built-in TypeScript Extension
|
| 18 |
+
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
| 19 |
+
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
| 20 |
+
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Translator</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="app"></div>
|
| 11 |
+
<script type="module" src="/src/main.ts"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c21edb05647a0a642fafa82eb17c27c83b4cedcc03cda590eb0e16200a8e3ac4
|
| 3 |
+
size 1218
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:190c877db466995bf1482f4a16abd06e04a89ede3119341e2a86ff96e1737b27
|
| 3 |
+
size 80
|
frontend/public/favicon.ico
ADDED
|
|
Git LFS Details
|
frontend/src/App.vue
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<template>
|
| 2 |
+
<!-- <Header/> -->
|
| 3 |
+
<router-view class="content" />
|
| 4 |
+
<!-- <Footer/> -->
|
| 5 |
+
|
| 6 |
+
<!-- <a-layout>
|
| 7 |
+
<a-layout-sider :style="siderStyle" width="300" :collapsed-width="0" :collapsed="!sider_open" >
|
| 8 |
+
Sider
|
| 9 |
+
<a-button @click="toggleSider">close</a-button>
|
| 10 |
+
</a-layout-sider>
|
| 11 |
+
<a-layout>
|
| 12 |
+
<router-view class="content" />
|
| 13 |
+
</a-layout>
|
| 14 |
+
</a-layout> -->
|
| 15 |
+
|
| 16 |
+
</template>
|
| 17 |
+
|
| 18 |
+
<script setup lang="ts">
|
| 19 |
+
import Header from "@/views/Header.vue";
|
| 20 |
+
import Footer from "@/views/Footer.vue";
|
| 21 |
+
|
| 22 |
+
// import * as api from "@/client";
|
| 23 |
+
import { onBeforeMount, onMounted, watch, CSSProperties, ref} from "vue";
|
| 24 |
+
import {useSettingsStore} from "@/stores/config.ts";
|
| 25 |
+
|
| 26 |
+
import axios from "axios";
|
| 27 |
+
import {getRandomNumInt} from "@/utils/size.ts";
|
| 28 |
+
|
| 29 |
+
const base_url = axios.defaults.baseURL
|
| 30 |
+
|
| 31 |
+
const settingsStore = useSettingsStore();
|
| 32 |
+
|
| 33 |
+
const sider_open = ref(settingsStore.$state.sider_open);
|
| 34 |
+
const siderStyle: CSSProperties = {
|
| 35 |
+
textAlign: 'center',
|
| 36 |
+
lineHeight: '90px',
|
| 37 |
+
color: '#fff',
|
| 38 |
+
backgroundColor: '#3ba0e9',
|
| 39 |
+
};
|
| 40 |
+
|
| 41 |
+
watch(() => settingsStore.$state.sider_open, (newVal) => {
|
| 42 |
+
sider_open.value = newVal;
|
| 43 |
+
console.log('sider open changed: ', sider_open.value);
|
| 44 |
+
});
|
| 45 |
+
|
| 46 |
+
const toggleSider = () => {
|
| 47 |
+
sider_open.value = !sider_open.value;
|
| 48 |
+
settingsStore.$patch({sider_open: sider_open.value});
|
| 49 |
+
console.log('sider open: ', sider_open.value);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// const registerSession = async () => {
|
| 53 |
+
// console.log('register ...')
|
| 54 |
+
// const role = settingsStore.$state.role_name
|
| 55 |
+
|
| 56 |
+
// const response = await fetch(`${base_url}/register?role=${role}`)
|
| 57 |
+
// const res = await response.json()
|
| 58 |
+
// console.log('res: ', res)
|
| 59 |
+
// return res['session_id']
|
| 60 |
+
// }
|
| 61 |
+
|
| 62 |
+
// watch(() => settingsStore.$state.role_name, async (role_name: any) => {
|
| 63 |
+
// console.log('>>>>> role updated', role_name)
|
| 64 |
+
// let session_id = await registerSession()
|
| 65 |
+
// if (!session_id) {
|
| 66 |
+
// console.log('register session failed')
|
| 67 |
+
// session_id = getRandomNumInt(100000, 999999)
|
| 68 |
+
// }
|
| 69 |
+
// // @ts-ignore
|
| 70 |
+
// sessionsStore.$patch({current_session_id: session_id + ''})
|
| 71 |
+
// console.log('session id: ', sessionsStore.$state.current_session_id)
|
| 72 |
+
// })
|
| 73 |
+
|
| 74 |
+
onMounted(async () => {
|
| 75 |
+
// console.log('app mounted', settingsStore.$state)
|
| 76 |
+
|
| 77 |
+
// let session_id = await registerSession()
|
| 78 |
+
// if (!session_id) {
|
| 79 |
+
// console.log('register session failed')
|
| 80 |
+
// session_id = getRandomNumInt(100000, 999999)
|
| 81 |
+
// }
|
| 82 |
+
// // @ts-ignore
|
| 83 |
+
// sessionsStore.$patch({current_session_id: session_id + ''})
|
| 84 |
+
// console.log('session id: ', sessionsStore.$state.current_session_id)
|
| 85 |
+
})
|
| 86 |
+
|
| 87 |
+
</script>
|
| 88 |
+
|
| 89 |
+
<style scoped>
|
| 90 |
+
.content {
|
| 91 |
+
background-color: white;
|
| 92 |
+
/* max-width: 1280px;
|
| 93 |
+
min-height: 720px; */
|
| 94 |
+
margin: 0 auto;
|
| 95 |
+
display: flex;
|
| 96 |
+
flex-direction: column;
|
| 97 |
+
align-items: center;
|
| 98 |
+
justify-content: space-between;
|
| 99 |
+
}
|
| 100 |
+
</style>
|
frontend/src/assets/ball.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:edd650ec984e26b5fde217f273e6758d0862fc856b5333e678fa0b578374e8b9
|
| 3 |
+
size 23084
|
frontend/src/assets/bg.png
ADDED
|
Git LFS Details
|
frontend/src/assets/close.png
ADDED
|
Git LFS Details
|
frontend/src/assets/download.png
ADDED
|
Git LFS Details
|
frontend/src/assets/icon.png
ADDED
|
|
Git LFS Details
|
frontend/src/assets/microphone.png
ADDED
|
Git LFS Details
|
frontend/src/assets/microphone_off.png
ADDED
|
Git LFS Details
|
frontend/src/assets/setting.png
ADDED
|
Git LFS Details
|
frontend/src/assets/text.png
ADDED
|
Git LFS Details
|
frontend/src/assets/text_off.png
ADDED
|
Git LFS Details
|
frontend/src/config/axios/config.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
const config: {
|
| 4 |
+
base_url: {
|
| 5 |
+
base: string
|
| 6 |
+
dev: string
|
| 7 |
+
pro: string
|
| 8 |
+
test: string
|
| 9 |
+
}
|
| 10 |
+
result_code: number | string
|
| 11 |
+
default_headers: AxiosHeaders
|
| 12 |
+
request_timeout: number
|
| 13 |
+
} = {
|
| 14 |
+
/**
|
| 15 |
+
* api请求基础路径
|
| 16 |
+
*/
|
| 17 |
+
base_url: {
|
| 18 |
+
// 开发环境接口前缀
|
| 19 |
+
base: '',
|
| 20 |
+
|
| 21 |
+
// 打包开发环境接口前缀
|
| 22 |
+
dev: '',
|
| 23 |
+
|
| 24 |
+
// 打包生产环境接口前缀
|
| 25 |
+
pro: '',
|
| 26 |
+
|
| 27 |
+
// 打包测试环境接口前缀
|
| 28 |
+
test: ''
|
| 29 |
+
},
|
| 30 |
+
|
| 31 |
+
/**
|
| 32 |
+
* 接口成功返回状态码
|
| 33 |
+
*/
|
| 34 |
+
result_code: '0000',
|
| 35 |
+
|
| 36 |
+
/**
|
| 37 |
+
* 接口请求超时时间
|
| 38 |
+
*/
|
| 39 |
+
request_timeout: 60000,
|
| 40 |
+
|
| 41 |
+
/**
|
| 42 |
+
* 默认接口请求类型
|
| 43 |
+
* 可选值:application/x-www-form-urlencoded multipart/form-data
|
| 44 |
+
*/
|
| 45 |
+
default_headers: 'application/json'
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
export { config }
|
frontend/src/config/axios/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios, {
|
| 2 |
+
AxiosInstance,
|
| 3 |
+
AxiosRequestConfig,
|
| 4 |
+
AxiosRequestHeaders,
|
| 5 |
+
AxiosResponse,
|
| 6 |
+
AxiosError
|
| 7 |
+
} from 'axios'
|
| 8 |
+
|
| 9 |
+
import { notification } from 'ant-design-vue';
|
| 10 |
+
|
| 11 |
+
import qs from 'qs'
|
| 12 |
+
|
| 13 |
+
import { config } from '@/config/axios/config'
|
| 14 |
+
|
| 15 |
+
const { result_code, base_url } = config
|
| 16 |
+
|
| 17 |
+
// export const PATH_URL = base_url[import.meta.env.VITE_API_BASEPATH]
|
| 18 |
+
|
| 19 |
+
// export const PATH_URL = 'localhost:8000/'
|
| 20 |
+
export const PATH_URL = '/'
|
| 21 |
+
|
| 22 |
+
// 创建axios实例
|
| 23 |
+
const service: AxiosInstance = axios.create({
|
| 24 |
+
baseURL: PATH_URL, // api 的 base_url
|
| 25 |
+
timeout: config.request_timeout // 请求超时时间
|
| 26 |
+
})
|
| 27 |
+
|
| 28 |
+
// request拦截器
|
| 29 |
+
service.interceptors.request.use(
|
| 30 |
+
(config: any ) => {
|
| 31 |
+
if (
|
| 32 |
+
config.method === 'post' &&
|
| 33 |
+
(config.headers as AxiosRequestHeaders)['Content-Type'] ===
|
| 34 |
+
'application/x-www-form-urlencoded'
|
| 35 |
+
) {
|
| 36 |
+
config.data = qs.stringify(config.data)
|
| 37 |
+
}
|
| 38 |
+
// get参数编码
|
| 39 |
+
if (config.method === 'get' && config.params) {
|
| 40 |
+
let url = config.url as string
|
| 41 |
+
url += '?'
|
| 42 |
+
const keys = Object.keys(config.params)
|
| 43 |
+
for (const key of keys) {
|
| 44 |
+
if (config.params[key] !== void 0 && config.params[key] !== null) {
|
| 45 |
+
url += `${key}=${encodeURIComponent(config.params[key])}&`
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
url = url.substring(0, url.length - 1)
|
| 49 |
+
config.params = {}
|
| 50 |
+
config.url = url
|
| 51 |
+
}
|
| 52 |
+
return config
|
| 53 |
+
},
|
| 54 |
+
(error: AxiosError) => {
|
| 55 |
+
// Do something with request error
|
| 56 |
+
Promise.reject(error)
|
| 57 |
+
}
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
service.interceptors.response.use(
|
| 61 |
+
(response: any) => {
|
| 62 |
+
if (response.data.code === result_code) {
|
| 63 |
+
return response.data
|
| 64 |
+
} else {
|
| 65 |
+
notification.error(response.data.message)
|
| 66 |
+
}
|
| 67 |
+
},
|
| 68 |
+
(error: AxiosError) => {
|
| 69 |
+
console.log('err' + error) // for debug
|
| 70 |
+
notification.error(error)
|
| 71 |
+
return Promise.reject(error)
|
| 72 |
+
}
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
export { service }
|
frontend/src/config/client_config.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import axios, {AxiosResponse} from "axios";
|
| 2 |
+
import {useCache} from "@/hooks/useCache";
|
| 3 |
+
import {toggleError} from '@/hooks/showError'
|
| 4 |
+
import router from "@/router";
|
| 5 |
+
|
| 6 |
+
const { wsCache } = useCache();
|
| 7 |
+
|
| 8 |
+
export const test_server = '127.0.0.1:8848'
|
| 9 |
+
// export const test_server = '59.110.18.232:19001'
|
| 10 |
+
|
| 11 |
+
axios.defaults.baseURL = import.meta.env.PROD ? '/api/v1' : `http://${test_server}/api/v1`;
|
| 12 |
+
axios.interceptors.request.use(
|
| 13 |
+
(config: any) => {
|
| 14 |
+
// Do something before request is sent
|
| 15 |
+
const {wsCache} = useCache()
|
| 16 |
+
const token = wsCache.get('token')
|
| 17 |
+
if (token) {
|
| 18 |
+
//@ts-ignore
|
| 19 |
+
config.headers.Authorization = 'Bearer ' + token
|
| 20 |
+
}
|
| 21 |
+
return config;
|
| 22 |
+
}
|
| 23 |
+
)
|
frontend/src/hooks/showError.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { reactive} from 'vue'
|
| 2 |
+
const toggleError = reactive({show:false, title:'error',msg:'Failed to connect to server'})
|
| 3 |
+
export {toggleError}
|
frontend/src/hooks/useCache.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* 配置浏览器本地存储的方式,可直接存储对象数组。
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import WebStorageCache from 'web-storage-cache'
|
| 6 |
+
|
| 7 |
+
type CacheType = 'sessionStorage' | 'localStorage'
|
| 8 |
+
|
| 9 |
+
export const useCache = (type: CacheType = 'sessionStorage') => {
|
| 10 |
+
const wsCache: WebStorageCache = new WebStorageCache({
|
| 11 |
+
storage: type
|
| 12 |
+
})
|
| 13 |
+
|
| 14 |
+
return {
|
| 15 |
+
wsCache
|
| 16 |
+
}
|
| 17 |
+
}
|
frontend/src/index.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
| 2 |
+
declare module "*.txt" {
|
| 3 |
+
const content: string;
|
| 4 |
+
export default content;
|
| 5 |
+
}
|
| 6 |
+
declare module '*.vue' {
|
| 7 |
+
import { DefineComponent } from 'vue'
|
| 8 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
| 9 |
+
const component: DefineComponent<{}, {}, any>
|
| 10 |
+
export default component
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
declare module 'qs';
|
| 14 |
+
|
| 15 |
+
declare type Recordable<T = any, K = string> = Record<K extends null | undefined ? string : K, T>
|
| 16 |
+
|
| 17 |
+
declare type AxiosHeaders =
|
| 18 |
+
| 'application/json'
|
| 19 |
+
| 'application/x-www-form-urlencoded'
|
| 20 |
+
| 'multipart/form-data'
|
| 21 |
+
|
| 22 |
+
declare type AxiosMethod = 'get' | 'post' | 'delete' | 'put'
|
| 23 |
+
|
| 24 |
+
declare type AxiosResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream'
|
| 25 |
+
|
| 26 |
+
declare type AxiosConfig = {
|
| 27 |
+
params?: any
|
| 28 |
+
data?: any
|
| 29 |
+
url?: string
|
| 30 |
+
method?: AxiosMethod
|
| 31 |
+
headersType?: string
|
| 32 |
+
responseType?: AxiosResponseType
|
| 33 |
+
}
|
| 34 |
+
|
frontend/src/main.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createApp } from 'vue'
|
| 2 |
+
import Antd from 'ant-design-vue';
|
| 3 |
+
import './config/client_config'
|
| 4 |
+
import { createPinia } from 'pinia';
|
| 5 |
+
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
| 6 |
+
import Vue3Lottie from 'vue3-lottie'
|
| 7 |
+
import 'ant-design-vue/dist/reset.css';
|
| 8 |
+
import './style.scss'
|
| 9 |
+
|
| 10 |
+
import App from './App.vue'
|
| 11 |
+
import router from './router'
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
// import * as Sentry from "@sentry/browser";
|
| 15 |
+
//
|
| 16 |
+
// Sentry.init({
|
| 17 |
+
// dsn: "http://1f5e3e8958a24528b5030068902e177e@127.0.0.1:8543/7",
|
| 18 |
+
// debug: true,
|
| 19 |
+
// });
|
| 20 |
+
|
| 21 |
+
const pinia = createPinia();
|
| 22 |
+
pinia.use(piniaPluginPersistedstate);
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
createApp(App)
|
| 27 |
+
.use(pinia)
|
| 28 |
+
.use(router)
|
| 29 |
+
.use(Antd)
|
| 30 |
+
.use(Vue3Lottie)
|
| 31 |
+
.mount('#app')
|
frontend/src/router.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
| 2 |
+
|
| 3 |
+
import NotFoundVue from '@/views/404/index.vue';
|
| 4 |
+
import WelcomeVue from '@/views/Welcome/index.vue';
|
| 5 |
+
import HomeVue from '@/views/Home/index.vue';
|
| 6 |
+
import SettingsVue from '@/views/Settings/index.vue';
|
| 7 |
+
|
| 8 |
+
const routes: Array<RouteRecordRaw> = [
|
| 9 |
+
{
|
| 10 |
+
name:"welcome",
|
| 11 |
+
path: '/',
|
| 12 |
+
component: WelcomeVue,
|
| 13 |
+
meta: {
|
| 14 |
+
requiresAgreement: false,
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
{
|
| 18 |
+
name: "home",
|
| 19 |
+
path: '/home',
|
| 20 |
+
component: HomeVue,
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
name:"settings",
|
| 24 |
+
path:'/settings',
|
| 25 |
+
component: SettingsVue,
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
name:"404",
|
| 29 |
+
path:'/404',
|
| 30 |
+
component: NotFoundVue,
|
| 31 |
+
}
|
| 32 |
+
];
|
| 33 |
+
|
| 34 |
+
const router = createRouter({
|
| 35 |
+
// history: createWebHistory(),
|
| 36 |
+
history: createWebHistory('/app/'),
|
| 37 |
+
routes,
|
| 38 |
+
});
|
| 39 |
+
|
| 40 |
+
router.beforeEach((to, from, next) => {
|
| 41 |
+
console.log('=============== router to : ', to)
|
| 42 |
+
if (to.matched.length === 0) {
|
| 43 |
+
next({ name: '404' });
|
| 44 |
+
} else {
|
| 45 |
+
next();
|
| 46 |
+
}
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
export default router;
|
frontend/src/stores/config.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { defineStore } from 'pinia';
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
export const useSettingsStore = defineStore({
|
| 6 |
+
id: 'settings',
|
| 7 |
+
persist: true,
|
| 8 |
+
state: () => {
|
| 9 |
+
return {
|
| 10 |
+
role: '',
|
| 11 |
+
language: 'zh',
|
| 12 |
+
sider_open: true,
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
actions: {
|
| 16 |
+
}
|
| 17 |
+
});
|
| 18 |
+
|
frontend/src/stores/measure.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineStore } from 'pinia';
|
| 2 |
+
|
| 3 |
+
export const useMeasureStore = defineStore({
|
| 4 |
+
id: 'measure_store',
|
| 5 |
+
persist: false,
|
| 6 |
+
state: () => {
|
| 7 |
+
return {
|
| 8 |
+
first_request_time: 0,
|
| 9 |
+
first_response_time: 0,
|
| 10 |
+
}
|
| 11 |
+
},
|
| 12 |
+
actions: {
|
| 13 |
+
}
|
| 14 |
+
});
|
| 15 |
+
|
frontend/src/stores/session.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineStore } from 'pinia';
|
| 2 |
+
import { ref, computed } from 'vue';
|
| 3 |
+
|
| 4 |
+
// 定义会话中单个节点的结构 (如果比纯文本更复杂)
|
| 5 |
+
export interface SessionNode {
|
| 6 |
+
id: string; // 或者其他唯一标识符
|
| 7 |
+
text: string;
|
| 8 |
+
translatedText?: string; // 可选的翻译文本
|
| 9 |
+
timestamp: number; // 时间戳
|
| 10 |
+
// 可以添加其他元数据,如语言、说话人等
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
// 定义存储在 Pinia 和用于 Modal 列表的会话摘要结构
|
| 14 |
+
export interface SessionSummary {
|
| 15 |
+
startTime: number; // 作为唯一 ID 和排序依据
|
| 16 |
+
title: string; // 第一句话的前10个字
|
| 17 |
+
outline: string[]; // 前两行内容
|
| 18 |
+
nodeCount: number; // 会话中的节点总数
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
const LOCAL_STORAGE_SESSION_PREFIX = 'rt_session_'; // 本地存储键前缀
|
| 22 |
+
|
| 23 |
+
export const useSessionStore = defineStore('session', () => {
|
| 24 |
+
// --- State ---
|
| 25 |
+
|
| 26 |
+
// 会话摘要列表,将由 pinia-plugin-persistedstate 自动持久化
|
| 27 |
+
const sessionSummaries = ref<SessionSummary[]>([]);
|
| 28 |
+
|
| 29 |
+
// 当前活动会话的节点 (不持久化)
|
| 30 |
+
const currentSessionNodes = ref<SessionNode[]>([]);
|
| 31 |
+
// 当前活动会话的开始时间 (不持久化)
|
| 32 |
+
const currentSessionStartTime = ref<number | null>(null);
|
| 33 |
+
// 标记会话是否正在进行中 (不持久化)
|
| 34 |
+
const isSessionActive = ref(false);
|
| 35 |
+
|
| 36 |
+
// --- Getters ---
|
| 37 |
+
|
| 38 |
+
// 按开始时间降序排列的会话摘要
|
| 39 |
+
const sortedSessionSummaries = computed(() => {
|
| 40 |
+
// 创建副本进行排序,避免直接修改响应式 ref
|
| 41 |
+
return [...sessionSummaries.value].sort((a, b) => b.startTime - a.startTime);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
// --- Actions ---
|
| 45 |
+
|
| 46 |
+
/**
|
| 47 |
+
* 开始一个新的会话
|
| 48 |
+
*/
|
| 49 |
+
function startSession() {
|
| 50 |
+
if (isSessionActive.value) {
|
| 51 |
+
console.warn("尝试在已有活动会话时开始新会话。");
|
| 52 |
+
// 可以选择结束旧会话或直接返回
|
| 53 |
+
// endSession(); // 如果需要自动结束旧会话
|
| 54 |
+
return;
|
| 55 |
+
}
|
| 56 |
+
currentSessionStartTime.value = Date.now();
|
| 57 |
+
currentSessionNodes.value = [];
|
| 58 |
+
isSessionActive.value = true;
|
| 59 |
+
console.log(`新会话开始于: ${new Date(currentSessionStartTime.value).toLocaleString()}`);
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
/**
|
| 63 |
+
* 向当前活动会话添加一个节点
|
| 64 |
+
* @param node - 要添加的会话节点
|
| 65 |
+
*/
|
| 66 |
+
function addNode(node: SessionNode) {
|
| 67 |
+
if (!isSessionActive.value || !currentSessionStartTime.value) {
|
| 68 |
+
console.warn("没有活动的会话来添加节点。");
|
| 69 |
+
return;
|
| 70 |
+
}
|
| 71 |
+
currentSessionNodes.value.push(node);
|
| 72 |
+
// 可选:如果需要更强的容错性,可以在这里进行增量保存到 localStorage
|
| 73 |
+
// saveCurrentSessionToLocalStorage();
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/**
|
| 77 |
+
* 结束当前活动会话,保存完整内容到 localStorage,并更新摘要列表
|
| 78 |
+
*/
|
| 79 |
+
function endSession() {
|
| 80 |
+
if (!isSessionActive.value || !currentSessionStartTime.value) {
|
| 81 |
+
console.log("没有活动的会话可以结束。");
|
| 82 |
+
// 确保状态被重置
|
| 83 |
+
isSessionActive.value = false;
|
| 84 |
+
currentSessionStartTime.value = null;
|
| 85 |
+
currentSessionNodes.value = [];
|
| 86 |
+
return;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
const startTime = currentSessionStartTime.value;
|
| 90 |
+
const nodes = [...currentSessionNodes.value]; // 创建副本
|
| 91 |
+
|
| 92 |
+
// 重置当前会话状态
|
| 93 |
+
isSessionActive.value = false;
|
| 94 |
+
currentSessionStartTime.value = null;
|
| 95 |
+
currentSessionNodes.value = [];
|
| 96 |
+
|
| 97 |
+
if (nodes.length === 0) {
|
| 98 |
+
console.log("会话结束,但没有节点需要保存。");
|
| 99 |
+
return;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
// 1. 生成摘要信息
|
| 103 |
+
const title = nodes[0]?.text.substring(0, 10) || '无标题会话';
|
| 104 |
+
const n1 = nodes[0];
|
| 105 |
+
const outline = [
|
| 106 |
+
`${n1?.text.substring(0, 56)}...\n`,
|
| 107 |
+
`${n1?.translatedText?.substring(0, 56)}...\n`,
|
| 108 |
+
]
|
| 109 |
+
// `${n1?.text.substring(0, 56)}\n${'-'.repeat(60)}\n${n1.translatedText?.substring(0, 56)}\n`
|
| 110 |
+
const summary: SessionSummary = {
|
| 111 |
+
startTime,
|
| 112 |
+
title,
|
| 113 |
+
outline,
|
| 114 |
+
nodeCount: nodes.length,
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
// 2. 保存完整会话内容到 Local Storage
|
| 118 |
+
try {
|
| 119 |
+
const storageKey = `${LOCAL_STORAGE_SESSION_PREFIX}${startTime}`;
|
| 120 |
+
localStorage.setItem(storageKey, JSON.stringify(nodes));
|
| 121 |
+
console.log(`完整会话 ${startTime} 已保存到 localStorage.`);
|
| 122 |
+
|
| 123 |
+
// 3. 更新 Pinia 中的摘要列表
|
| 124 |
+
// 检查是否已存在相同 startTime 的摘要 (理论上不应发生,除非手动操作或错误)
|
| 125 |
+
const existingIndex = sessionSummaries.value.findIndex(s => s.startTime === startTime);
|
| 126 |
+
if (existingIndex === -1) {
|
| 127 |
+
sessionSummaries.value.push(summary);
|
| 128 |
+
} else {
|
| 129 |
+
console.warn(`会话摘要 ${startTime} 已存在,将进行覆盖��`);
|
| 130 |
+
sessionSummaries.value[existingIndex] = summary;
|
| 131 |
+
}
|
| 132 |
+
// pinia-plugin-persistedstate 会自动处理 sessionSummaries 的持久化
|
| 133 |
+
|
| 134 |
+
console.log(`会话 ${startTime} 结束并已处理。`);
|
| 135 |
+
|
| 136 |
+
} catch (error) {
|
| 137 |
+
console.error("保存会话到 localStorage 时出错:", error);
|
| 138 |
+
// 这里可以添加用户反馈,例如提示存储空间不足
|
| 139 |
+
// 也许需要决定是否回滚摘要列表的添加
|
| 140 |
+
}
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
/**
|
| 144 |
+
* 从 Local Storage 加载指定会话的完整内容
|
| 145 |
+
* @param startTime - 会话的开始时间戳 (作为 ID)
|
| 146 |
+
* @returns SessionNode[] | null - 会话节点数组或在未找到/出错时返回 null
|
| 147 |
+
*/
|
| 148 |
+
function loadSessionContent(startTime: number): SessionNode[] | null {
|
| 149 |
+
try {
|
| 150 |
+
const storageKey = `${LOCAL_STORAGE_SESSION_PREFIX}${startTime}`;
|
| 151 |
+
const storedData = localStorage.getItem(storageKey);
|
| 152 |
+
if (storedData) {
|
| 153 |
+
const nodes = JSON.parse(storedData) as SessionNode[];
|
| 154 |
+
console.log(`从 localStorage 加载了会话 ${startTime} 的内容 (${nodes.length} 个节点)`);
|
| 155 |
+
return nodes;
|
| 156 |
+
}
|
| 157 |
+
console.warn(`在 localStorage 中未找到键为 ${storageKey} 的会话数据。`);
|
| 158 |
+
return null;
|
| 159 |
+
} catch (error) {
|
| 160 |
+
console.error(`从 localStorage 加载会话 ${startTime} 时出错:`, error);
|
| 161 |
+
return null;
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
/**
|
| 166 |
+
* 删除指定的会话 (包括摘要和本地存储的完整内容)
|
| 167 |
+
* @param startTime - 要删除的会话的开始时间戳
|
| 168 |
+
*/
|
| 169 |
+
function deleteSession(startTime: number) {
|
| 170 |
+
try {
|
| 171 |
+
// 1. 从摘要列表中移除
|
| 172 |
+
const index = sessionSummaries.value.findIndex(s => s.startTime === startTime);
|
| 173 |
+
if (index > -1) {
|
| 174 |
+
sessionSummaries.value.splice(index, 1);
|
| 175 |
+
console.log(`会话摘要 ${startTime} 已从 Pinia store 中移除。`);
|
| 176 |
+
// pinia-plugin-persistedstate 会自动更新持久化的摘要列表
|
| 177 |
+
} else {
|
| 178 |
+
console.warn(`尝试删除一个不存在的会话摘要: ${startTime}`);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
// 2. 从 Local Storage 中移除完整内容
|
| 182 |
+
const storageKey = `${LOCAL_STORAGE_SESSION_PREFIX}${startTime}`;
|
| 183 |
+
localStorage.removeItem(storageKey);
|
| 184 |
+
console.log(`会话 ${startTime} 的完整内容已从 localStorage 中移除。`);
|
| 185 |
+
|
| 186 |
+
} catch (error) {
|
| 187 |
+
console.error(`删除会话 ${startTime} 时出错:`, error);
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
// --- 返回 State, Getters, Actions ---
|
| 192 |
+
return {
|
| 193 |
+
// State
|
| 194 |
+
sessionSummaries, // 摘要列表 (将被持久化)
|
| 195 |
+
currentSessionNodes, // 当前活动会话的节点 (用于可能的实时显示)
|
| 196 |
+
currentSessionStartTime, // 当前活动会话的开始时间
|
| 197 |
+
isSessionActive, // 会话是否活动
|
| 198 |
+
|
| 199 |
+
// Getters
|
| 200 |
+
sortedSessionSummaries, // 排序后的摘要列表
|
| 201 |
+
|
| 202 |
+
// Actions
|
| 203 |
+
startSession,
|
| 204 |
+
addNode,
|
| 205 |
+
endSession,
|
| 206 |
+
loadSessionContent, // 用于下载按钮点击时加载数据
|
| 207 |
+
deleteSession,
|
| 208 |
+
};
|
| 209 |
+
}, {
|
| 210 |
+
// Pinia 持久化配置
|
| 211 |
+
persist: {
|
| 212 |
+
// 只持久化 sessionSummaries 状态
|
| 213 |
+
paths: ['sessionSummaries'],
|
| 214 |
+
// 默认使用 localStorage,如果需要可以指定
|
| 215 |
+
// storage: localStorage,
|
| 216 |
+
},
|
| 217 |
+
});
|
| 218 |
+
|
| 219 |
+
/**
|
| 220 |
+
* 辅助函数:触发浏览器下载会话数据
|
| 221 |
+
* @param startTime - 会话开始时间,用于文件名
|
| 222 |
+
* @param nodes - 要下载的会话节点数据
|
| 223 |
+
* @param format - 'json' 或 'txt' (默认为 'json')
|
| 224 |
+
*/
|
| 225 |
+
export function downloadSessionData(startTime: number, nodes: SessionNode[], format: 'json' | 'txt' = 'json') {
|
| 226 |
+
if (!nodes || nodes.length === 0) {
|
| 227 |
+
console.error("没有数据可供下载:", startTime);
|
| 228 |
+
alert("没有内容可以下载。"); // 给用户反馈
|
| 229 |
+
return;
|
| 230 |
+
}
|
| 231 |
+
try {
|
| 232 |
+
const dateStr = new Date(startTime).toISOString().split('T')[0]; // YYYY-MM-DD
|
| 233 |
+
let dataStr: string;
|
| 234 |
+
let mimeType: string;
|
| 235 |
+
let fileExtension: string;
|
| 236 |
+
|
| 237 |
+
if (format === 'txt') {
|
| 238 |
+
dataStr = nodes.map(n => `${new Date(n.timestamp).toLocaleTimeString()} - ${n.text}`).join('\n');
|
| 239 |
+
mimeType = 'text/plain;charset=utf-8;';
|
| 240 |
+
fileExtension = 'txt';
|
| 241 |
+
} else { // 默认为 json
|
| 242 |
+
dataStr = JSON.stringify(nodes, null, 2); // 美化 JSON 输出
|
| 243 |
+
mimeType = 'application/json;charset=utf-8;';
|
| 244 |
+
fileExtension = 'json';
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
const filename = `session_${dateStr}_${startTime}.${fileExtension}`;
|
| 248 |
+
const blob = new Blob([dataStr], { type: mimeType });
|
| 249 |
+
const link = document.createElement("a");
|
| 250 |
+
|
| 251 |
+
// 使用 createObjectURL 创建一个临时的 URL 指向 Blob 对象
|
| 252 |
+
const url = URL.createObjectURL(blob);
|
| 253 |
+
link.setAttribute("href", url);
|
| 254 |
+
link.setAttribute("download", filename);
|
| 255 |
+
link.style.visibility = 'hidden';
|
| 256 |
+
document.body.appendChild(link);
|
| 257 |
+
link.click(); // 模拟点击下载链接
|
| 258 |
+
|
| 259 |
+
// 清理:移除链接并释放 URL 对象
|
| 260 |
+
document.body.removeChild(link);
|
| 261 |
+
URL.revokeObjectURL(url);
|
| 262 |
+
|
| 263 |
+
console.log(`已触发下载会话 ${startTime} 为 ${filename}`);
|
| 264 |
+
|
| 265 |
+
} catch (error) {
|
| 266 |
+
console.error(`下载会话 ${startTime} 时出错:`, error);
|
| 267 |
+
alert("下载文件时发生错误。"); // 给用户反馈
|
| 268 |
+
}
|
| 269 |
+
}
|
frontend/src/style.scss
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
| 3 |
+
line-height: 1.5;
|
| 4 |
+
font-weight: 400;
|
| 5 |
+
|
| 6 |
+
color-scheme: light dark;
|
| 7 |
+
color: rgba(255, 255, 255, 0.87);
|
| 8 |
+
background-color: #242424;
|
| 9 |
+
|
| 10 |
+
font-synthesis: none;
|
| 11 |
+
text-rendering: optimizeLegibility;
|
| 12 |
+
-webkit-font-smoothing: antialiased;
|
| 13 |
+
-moz-osx-font-smoothing: grayscale;
|
| 14 |
+
-webkit-text-size-adjust: 100%;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
a {
|
| 18 |
+
font-weight: 500;
|
| 19 |
+
color: #646cff;
|
| 20 |
+
text-decoration: inherit;
|
| 21 |
+
}
|
| 22 |
+
a:hover {
|
| 23 |
+
color: #535bf2;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
body {
|
| 27 |
+
margin: 0;
|
| 28 |
+
display: flex;
|
| 29 |
+
place-items: center;
|
| 30 |
+
min-width: 320px;
|
| 31 |
+
height: 100%;
|
| 32 |
+
min-height: auto;
|
| 33 |
+
color: #333;
|
| 34 |
+
background: #fff;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h1 {
|
| 38 |
+
font-size: 3.2em;
|
| 39 |
+
line-height: 1.1;
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
button {
|
| 43 |
+
border-radius: 8px;
|
| 44 |
+
border: 1px solid transparent;
|
| 45 |
+
padding: 0.6em 1.2em;
|
| 46 |
+
font-size: 1em;
|
| 47 |
+
font-weight: 500;
|
| 48 |
+
font-family: inherit;
|
| 49 |
+
background-color: #1a1a1a;
|
| 50 |
+
cursor: pointer;
|
| 51 |
+
transition: border-color 0.25s;
|
| 52 |
+
}
|
| 53 |
+
//button:hover {
|
| 54 |
+
// border-color: #646cff;
|
| 55 |
+
//}
|
| 56 |
+
//button:focus,
|
| 57 |
+
//button:focus-visible {
|
| 58 |
+
// outline: 4px auto -webkit-focus-ring-color;
|
| 59 |
+
//}
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
$FormMaxWidth: 1024px;
|
| 63 |
+
$FormItemWidth: 1022px;
|
| 64 |
+
|
| 65 |
+
.card {
|
| 66 |
+
border-bottom: solid 2px lightgray;
|
| 67 |
+
//border-radius: 4px;
|
| 68 |
+
align-items: center;
|
| 69 |
+
justify-content: center;
|
| 70 |
+
/* padding: 2em; */
|
| 71 |
+
margin-top: 40px;
|
| 72 |
+
display: flex;
|
| 73 |
+
max-width: $FormMaxWidth;
|
| 74 |
+
width: 100%;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.seg-title {
|
| 78 |
+
margin: 24px 0;
|
| 79 |
+
font-size: 20px;
|
| 80 |
+
font-weight: 500;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.seg-co {
|
| 84 |
+
width: 1022px;
|
| 85 |
+
text-align: left;
|
| 86 |
+
border-left: solid 6px midnightblue;
|
| 87 |
+
padding-left: 8px;
|
| 88 |
+
margin-left: 2px;
|
| 89 |
+
margin-top: 36px;
|
| 90 |
+
line-height: 24px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
#app {
|
| 94 |
+
margin: 0 auto;
|
| 95 |
+
padding: 0 ;
|
| 96 |
+
text-align: center;
|
| 97 |
+
width: 100%;
|
| 98 |
+
height: 100%;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.ant-btn {
|
| 102 |
+
padding: 4px 12px;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
@media (prefers-color-scheme: light) {
|
| 106 |
+
:root {
|
| 107 |
+
color: #213547;
|
| 108 |
+
background-color: #ffffff;
|
| 109 |
+
}
|
| 110 |
+
a:hover {
|
| 111 |
+
color: #747bff;
|
| 112 |
+
}
|
| 113 |
+
button {
|
| 114 |
+
background-color: #f9f9f9;
|
| 115 |
+
}
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.ant-card {
|
| 119 |
+
background: #f5f6fa;
|
| 120 |
+
height: 100%;
|
| 121 |
+
}
|
| 122 |
+
.ant-card-body {
|
| 123 |
+
padding: 24px 36px 12px 36px !important;
|
| 124 |
+
border-radius: 0 0 8px 8px;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
.ant-card .ant-card-actions {
|
| 128 |
+
background-color: rgba(232, 232, 248, 0.8) !important;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.ant-popover {
|
| 132 |
+
max-width: 800px !important;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.ant-form-item {
|
| 136 |
+
background: transparent;
|
| 137 |
+
margin-bottom: 40px !important;
|
| 138 |
+
.ant-form-item-explain-error {
|
| 139 |
+
color: #ff4d4f;
|
| 140 |
+
text-align: left !important;
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.ant-form-item-label {
|
| 145 |
+
label {
|
| 146 |
+
font-size: 18px !important;
|
| 147 |
+
color: #1a1a1a !important;
|
| 148 |
+
font-weight: 500 !important;
|
| 149 |
+
}
|
| 150 |
+
}
|
| 151 |
+
.ant-tooltip {
|
| 152 |
+
max-width: $FormItemWidth !important;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.ant-page-header-heading {
|
| 156 |
+
width: 1022px !important;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.highlight {
|
| 160 |
+
//background: #feffe6;
|
| 161 |
+
background: ghostwhite;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
/* 折叠时隐藏整个 Sider */
|
| 165 |
+
.ant-layout-sider-collapsed {
|
| 166 |
+
width: 0 !important;
|
| 167 |
+
min-width: 0 !important;
|
| 168 |
+
overflow: hidden;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
/* 隐藏折叠后的图标栏 */
|
| 172 |
+
.ant-layout-sider-collapsed .ant-menu-item,
|
| 173 |
+
.ant-layout-sider-collapsed .ant-menu-submenu-title {
|
| 174 |
+
display: none;
|
| 175 |
+
}
|
frontend/src/utils/audio_utils.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// AudioUtils.ts
|
| 2 |
+
export class AudioUtils {
|
| 3 |
+
static async createPCM16Data(audioBuffer: AudioBuffer): Promise<ArrayBuffer> {
|
| 4 |
+
const numChannels = 1; // Mono
|
| 5 |
+
const sampleRate = 16000; // Target sample rate
|
| 6 |
+
const format = 1; // PCM
|
| 7 |
+
const bitDepth = 16;
|
| 8 |
+
|
| 9 |
+
// Resample if needed
|
| 10 |
+
let samples = audioBuffer.getChannelData(0);
|
| 11 |
+
if (audioBuffer.sampleRate !== sampleRate) {
|
| 12 |
+
samples = await this.resampleAudio(samples, audioBuffer.sampleRate, sampleRate);
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const dataLength = samples.length * (bitDepth / 8);
|
| 16 |
+
const headerLength = 44;
|
| 17 |
+
const totalLength = headerLength + dataLength;
|
| 18 |
+
|
| 19 |
+
const buffer = new ArrayBuffer(totalLength);
|
| 20 |
+
const view = new DataView(buffer);
|
| 21 |
+
|
| 22 |
+
// Write WAV header
|
| 23 |
+
this.writeString(view, 0, 'RIFF');
|
| 24 |
+
view.setUint32(4, totalLength - 8, true);
|
| 25 |
+
this.writeString(view, 8, 'WAVE');
|
| 26 |
+
this.writeString(view, 12, 'fmt ');
|
| 27 |
+
view.setUint32(16, 16, true);
|
| 28 |
+
view.setUint16(20, format, true);
|
| 29 |
+
view.setUint16(22, numChannels, true);
|
| 30 |
+
view.setUint32(24, sampleRate, true);
|
| 31 |
+
view.setUint32(28, sampleRate * numChannels * (bitDepth / 8), true);
|
| 32 |
+
view.setUint16(32, numChannels * (bitDepth / 8), true);
|
| 33 |
+
view.setUint16(34, bitDepth, true);
|
| 34 |
+
this.writeString(view, 36, 'data');
|
| 35 |
+
view.setUint32(40, dataLength, true);
|
| 36 |
+
|
| 37 |
+
// Write audio data
|
| 38 |
+
this.floatTo16BitPCM(view, 44, samples);
|
| 39 |
+
|
| 40 |
+
return buffer;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
static writeString(view: DataView, offset: number, string: string): void {
|
| 44 |
+
for (let i = 0; i < string.length; i++) {
|
| 45 |
+
view.setUint8(offset + i, string.charCodeAt(i));
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
static floatTo16BitPCM(view: DataView, offset: number, input: Float32Array): void {
|
| 50 |
+
for (let i = 0; i < input.length; i++, offset += 2) {
|
| 51 |
+
const s = Math.max(-1, Math.min(1, input[i]));
|
| 52 |
+
view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
static async resampleAudio(
|
| 57 |
+
audioData: Float32Array,
|
| 58 |
+
originalSampleRate: number,
|
| 59 |
+
targetSampleRate: number
|
| 60 |
+
): Promise<Float32Array> {
|
| 61 |
+
const originalLength = audioData.length;
|
| 62 |
+
const ratio = targetSampleRate / originalSampleRate;
|
| 63 |
+
const newLength = Math.round(originalLength * ratio);
|
| 64 |
+
const result = new Float32Array(newLength);
|
| 65 |
+
|
| 66 |
+
for (let i = 0; i < newLength; i++) {
|
| 67 |
+
const position = i / ratio;
|
| 68 |
+
const index = Math.floor(position);
|
| 69 |
+
const fraction = position - index;
|
| 70 |
+
|
| 71 |
+
if (index + 1 < originalLength) {
|
| 72 |
+
result[i] = audioData[index] * (1 - fraction) + audioData[index + 1] * fraction;
|
| 73 |
+
} else {
|
| 74 |
+
result[i] = audioData[index];
|
| 75 |
+
}
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
return result;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
export default AudioUtils;
|
frontend/src/utils/retry.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const retryAsyncFn = async (fn: any, retry: number) => {
|
| 2 |
+
try {
|
| 3 |
+
await fn()
|
| 4 |
+
} catch (e) {
|
| 5 |
+
if (retry > 0) {
|
| 6 |
+
setTimeout(async () => {
|
| 7 |
+
await retryAsyncFn(fn, retry - 1)
|
| 8 |
+
}, 500)
|
| 9 |
+
} else {
|
| 10 |
+
throw e
|
| 11 |
+
}
|
| 12 |
+
}
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export const retryFn = (fn: any, retry: number) => {
|
| 16 |
+
try {
|
| 17 |
+
fn()
|
| 18 |
+
} catch (e) {
|
| 19 |
+
if (retry > 0) {
|
| 20 |
+
setTimeout(() => {
|
| 21 |
+
retryFn(fn, retry - 1)
|
| 22 |
+
}, 500)
|
| 23 |
+
} else {
|
| 24 |
+
throw e
|
| 25 |
+
}
|
| 26 |
+
}
|
| 27 |
+
}
|
frontend/src/utils/size.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export const sizeCalculator = (size: number) => {
|
| 2 |
+
if (size < 1024) {
|
| 3 |
+
return size + ' B'
|
| 4 |
+
} else if (size < 1024 * 1024) {
|
| 5 |
+
return (size / 1024).toFixed(2) + ' KB'
|
| 6 |
+
} else if (size < 1024 * 1024 * 1024) {
|
| 7 |
+
return (size / 1024 / 1024).toFixed(2) + ' MB'
|
| 8 |
+
} else {
|
| 9 |
+
return (size / 1024 / 1024 / 1024).toFixed(2) + ' GB'
|
| 10 |
+
}
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export const getRandomNumInt = (min: number, max: number) => {
|
| 14 |
+
var Range = max - min;
|
| 15 |
+
var Rand = Math.random(); //获取[0-1)的随机数
|
| 16 |
+
return (min + Math.round(Rand * Range)); //放大取整
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export const formatMs = (ms:any ,all: any) => {
|
| 20 |
+
let ss=ms%1000;ms=(ms-ss)/1000;
|
| 21 |
+
let s=ms%60;ms=(ms-s)/60;
|
| 22 |
+
let m=ms%60;ms=(ms-m)/60;
|
| 23 |
+
let h=ms;
|
| 24 |
+
let t=(h?h+":":"")
|
| 25 |
+
+(all||h+m?("0"+m).substr(-2)+":":"")
|
| 26 |
+
+(all||h+m+s?("0"+s).substr(-2)+"″":"")
|
| 27 |
+
+("00"+ss).substr(-3);
|
| 28 |
+
return t;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
export const getRandomItems = (arr: [], num: number) => {
|
| 32 |
+
if (arr.length < num) {
|
| 33 |
+
throw new Error('The array does not contain enough elements.');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
// 复制原数组,避免修改原数组
|
| 37 |
+
let tempArray = [...arr];
|
| 38 |
+
|
| 39 |
+
// 打乱数组
|
| 40 |
+
for (let i = tempArray.length - 1; i > 0; i--) {
|
| 41 |
+
const j = Math.floor(Math.random() * (i + 1));
|
| 42 |
+
[tempArray[i], tempArray[j]] = [tempArray[j], tempArray[i]]; // ES6 的解构赋值来交换元素
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// 返回前num个项目
|
| 46 |
+
return tempArray.slice(0, num);
|
| 47 |
+
}
|
frontend/src/views/404/index.vue
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
|
| 3 |
+
import router from "@/router.ts";
|
| 4 |
+
|
| 5 |
+
const backAction = () => {
|
| 6 |
+
router.replace('/')
|
| 7 |
+
}
|
| 8 |
+
</script>
|
| 9 |
+
|
| 10 |
+
<template>
|
| 11 |
+
<div class="not-found-wrapper">
|
| 12 |
+
<a-result status="404" title="404" sub-title="Sorry, the page you visited does not exist.">
|
| 13 |
+
<template #extra>
|
| 14 |
+
<a-button @click="backAction" type="primary">Back Home</a-button>
|
| 15 |
+
</template>
|
| 16 |
+
</a-result>
|
| 17 |
+
</div>
|
| 18 |
+
</template>
|
| 19 |
+
|
| 20 |
+
<style lang="scss" scoped>
|
| 21 |
+
.not-found-wrapper {
|
| 22 |
+
height: calc(100vh - 104px);
|
| 23 |
+
}
|
| 24 |
+
</style>
|
frontend/src/views/Footer.vue
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
</script>
|
| 3 |
+
|
| 4 |
+
<template>
|
| 5 |
+
<div class="right">
|
| 6 |
+
© 2025 MoYoYo Inc. All Rights Reserved.
|
| 7 |
+
</div>
|
| 8 |
+
</template>
|
| 9 |
+
|
| 10 |
+
<style scoped>
|
| 11 |
+
|
| 12 |
+
.right {
|
| 13 |
+
position: fixed;
|
| 14 |
+
bottom: 0;
|
| 15 |
+
width: 100%;
|
| 16 |
+
height: 40px;
|
| 17 |
+
line-height: 40px;
|
| 18 |
+
text-align: center;
|
| 19 |
+
font-size: 0.8em;
|
| 20 |
+
background: white;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
</style>
|
frontend/src/views/Header.vue
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script lang="ts" setup>
|
| 2 |
+
import router from "@/router.ts";
|
| 3 |
+
import {watch, ref, onMounted, onUnmounted} from "vue";
|
| 4 |
+
import {ArrowRightOutlined, DashboardOutlined,
|
| 5 |
+
ExperimentOutlined, SettingOutlined} from "@ant-design/icons-vue";
|
| 6 |
+
|
| 7 |
+
const windowWidth = ref(window.innerWidth)
|
| 8 |
+
|
| 9 |
+
onMounted(() => {
|
| 10 |
+
window.addEventListener('resize', () => {
|
| 11 |
+
windowWidth.value = window.innerWidth
|
| 12 |
+
})
|
| 13 |
+
})
|
| 14 |
+
onUnmounted(() => {
|
| 15 |
+
window.removeEventListener('resize', () => {
|
| 16 |
+
windowWidth.value = window.innerWidth
|
| 17 |
+
})
|
| 18 |
+
})
|
| 19 |
+
|
| 20 |
+
const sessionActive = ref(false)
|
| 21 |
+
const toolsActive = ref(false)
|
| 22 |
+
watch(router.currentRoute, (to, from) => {
|
| 23 |
+
console.log('router changed', to, from)
|
| 24 |
+
|
| 25 |
+
const sessionActivePath = ['/sessions', '/sessions/', '/form', '/form/', '/output']
|
| 26 |
+
|
| 27 |
+
if (sessionActivePath.includes(to.path)) {
|
| 28 |
+
sessionActive.value = true
|
| 29 |
+
toolsActive.value = false
|
| 30 |
+
return
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
const toolsActivePath = ['/tools', '/tools/', '/benchmark', '/benchmark/', '/benchmark_detail', '/benchmark_detail/']
|
| 34 |
+
if (toolsActivePath.includes(to.path)) {
|
| 35 |
+
sessionActive.value = false
|
| 36 |
+
toolsActive.value = true
|
| 37 |
+
return
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
sessionActive.value = false
|
| 41 |
+
toolsActive.value = false
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
const gotoSettings = (e: any) => {
|
| 45 |
+
e.preventDefault()
|
| 46 |
+
router.push('/settings')
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
const gotoProfiling = (e: any) => {
|
| 50 |
+
e.preventDefault()
|
| 51 |
+
router.push('/profiling')
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
const gotoHealthcheck = (e: any) => {
|
| 55 |
+
e.preventDefault()
|
| 56 |
+
router.push('/healthcheck')
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
</script>
|
| 60 |
+
<template>
|
| 61 |
+
<nav class="header-nav">
|
| 62 |
+
<div class="nav-left flex flex-row justify-between items-center">
|
| 63 |
+
<router-link to="/" >
|
| 64 |
+
<img
|
| 65 |
+
alt="logo"
|
| 66 |
+
src="/logo.webp"
|
| 67 |
+
class="logo"
|
| 68 |
+
/>
|
| 69 |
+
</router-link>
|
| 70 |
+
|
| 71 |
+
</div>
|
| 72 |
+
<div class="title">
|
| 73 |
+
<div v-if="windowWidth >= 1280">
|
| 74 |
+
<div class="primary">AVATAR
|
| 75 |
+
</div>
|
| 76 |
+
<div class="secondary">(power by AI)</div>
|
| 77 |
+
</div>
|
| 78 |
+
<h3 v-if="windowWidth < 1280"
|
| 79 |
+
class="text-3xl font-medium text-primary-dark dark:text-ternary-light hidden sm:block"
|
| 80 |
+
style="font-size: 22px; line-height: 60px; font-weight: 600;"
|
| 81 |
+
>AVATAR</h3>
|
| 82 |
+
</div>
|
| 83 |
+
<div class="nav-right flex flex-row justify-between items-center">
|
| 84 |
+
<!-- <a-button type="link" href="/">-->
|
| 85 |
+
<!-- <span :style="router.currentRoute.value.path == '/' ? 'border-bottom: solid 2px; ' : ''" >Home</span>-->
|
| 86 |
+
<!-- </a-button>-->
|
| 87 |
+
<!-- <router-link to="/settings" >-->
|
| 88 |
+
<!-- <img-->
|
| 89 |
+
<!-- alt="logo"-->
|
| 90 |
+
<!-- src="/logo.webp"-->
|
| 91 |
+
<!-- class="logo"-->
|
| 92 |
+
<!-- />-->
|
| 93 |
+
<!-- </router-link>-->
|
| 94 |
+
<a-button type="ghost" style="margin-right: 12px;"
|
| 95 |
+
size="large" @click="gotoProfiling">
|
| 96 |
+
<template #icon>
|
| 97 |
+
<ExperimentOutlined style="font-size: 24px;"/>
|
| 98 |
+
</template>
|
| 99 |
+
</a-button>
|
| 100 |
+
<a-button type="ghost" style="margin-right: 12px;"
|
| 101 |
+
size="large" @click="gotoHealthcheck">
|
| 102 |
+
<template #icon>
|
| 103 |
+
<DashboardOutlined style="font-size: 24px;"/>
|
| 104 |
+
</template>
|
| 105 |
+
</a-button>
|
| 106 |
+
<a-button type="ghost" size="large" @click="gotoSettings">
|
| 107 |
+
<template #icon>
|
| 108 |
+
<SettingOutlined style="font-size: 24px;"/>
|
| 109 |
+
</template>
|
| 110 |
+
</a-button>
|
| 111 |
+
<!-- <a-button type="link" target="_blank" >-->
|
| 112 |
+
<!-- <span>Contact</span>-->
|
| 113 |
+
<!-- </a-button>-->
|
| 114 |
+
</div>
|
| 115 |
+
</nav>
|
| 116 |
+
</template>
|
| 117 |
+
<style scoped lang="scss">
|
| 118 |
+
.header-nav {
|
| 119 |
+
display: flex;
|
| 120 |
+
align-items: center;
|
| 121 |
+
justify-content: space-between;
|
| 122 |
+
width: 100vw;
|
| 123 |
+
height: 72px;
|
| 124 |
+
background: aliceblue;
|
| 125 |
+
box-shadow: 1px 1px 2px 1px #d9d9d9;
|
| 126 |
+
top: 0;
|
| 127 |
+
position: sticky;
|
| 128 |
+
z-index: 99;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.nav-left {
|
| 132 |
+
position: absolute;
|
| 133 |
+
left: 20px;
|
| 134 |
+
top: 12px;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.nav-right {
|
| 138 |
+
.ant-btn {
|
| 139 |
+
span {
|
| 140 |
+
font-size: 15px;
|
| 141 |
+
font-weight: 600;
|
| 142 |
+
}
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
.logo {
|
| 147 |
+
width: 48px;
|
| 148 |
+
height: 48px;
|
| 149 |
+
border-radius: 24px;
|
| 150 |
+
will-change: filter;
|
| 151 |
+
transition: filter 300ms;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.logo:hover {
|
| 155 |
+
filter: drop-shadow(0 0 2em #646cffaa);
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.logo.vue:hover {
|
| 159 |
+
filter: drop-shadow(0 0 2em #42b883aa);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.nav-items {
|
| 163 |
+
margin-left: 10px;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
.nav-right {
|
| 167 |
+
position: absolute;
|
| 168 |
+
right: 20px;
|
| 169 |
+
height: 64px;
|
| 170 |
+
align-items: center;
|
| 171 |
+
display: flex;
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
.title {
|
| 175 |
+
margin-left: 84px;
|
| 176 |
+
line-height: 48px;
|
| 177 |
+
height: 54px;
|
| 178 |
+
|
| 179 |
+
.primary {
|
| 180 |
+
font-weight: bold;
|
| 181 |
+
text-align: left;
|
| 182 |
+
font-size: 16px;
|
| 183 |
+
letter-spacing: -0.8px;
|
| 184 |
+
font-family: Courier,Menlo, monospace, 'SFMono-Regular', Consolas, 'Liberation Mono';
|
| 185 |
+
}
|
| 186 |
+
.secondary {
|
| 187 |
+
font-family: Courier,Menlo, monospace, 'SFMono-Regular', Consolas, 'Liberation Mono';
|
| 188 |
+
text-align: left;
|
| 189 |
+
font-size: 8px;
|
| 190 |
+
letter-spacing: -0.2px;
|
| 191 |
+
font-weight: 400;
|
| 192 |
+
line-height: 20px;
|
| 193 |
+
margin-top: -12px;
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.switch-icon {
|
| 198 |
+
margin-left: 20px;
|
| 199 |
+
}
|
| 200 |
+
</style>
|
frontend/src/views/Home/Components/ChatText.vue
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
|
| 3 |
+
import router from "@/router.ts";
|
| 4 |
+
import { useSettingsStore } from "@/stores/config.ts";
|
| 5 |
+
import { onMounted, ref, nextTick, watch, defineProps } from "vue";
|
| 6 |
+
|
| 7 |
+
const props = defineProps({
|
| 8 |
+
isPlaying: {
|
| 9 |
+
type: Boolean,
|
| 10 |
+
default: true
|
| 11 |
+
},
|
| 12 |
+
chatContent: {
|
| 13 |
+
type: [Object],
|
| 14 |
+
default: []
|
| 15 |
+
}
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
+
const contentListRef = ref(null);
|
| 19 |
+
// 自动滚动到底部的函数
|
| 20 |
+
const scrollToBottom = () => {
|
| 21 |
+
nextTick(() => {
|
| 22 |
+
if (contentListRef.value) {
|
| 23 |
+
// @ts-ignore
|
| 24 |
+
contentListRef.value.scrollTop = contentListRef.value.scrollHeight + 24;
|
| 25 |
+
}
|
| 26 |
+
});
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
// 监听 chatContent 的变化,自动滚动到底部, 具体list里面的某一条内容的长度改变了,或者list的长度改变了,也需要刷新
|
| 30 |
+
watch(() => props.chatContent, (newVal, oldVal) => {
|
| 31 |
+
// console.log('chatContent , auto scroll to bottom.....', newVal);
|
| 32 |
+
scrollToBottom();
|
| 33 |
+
}, { deep: true });
|
| 34 |
+
</script>
|
| 35 |
+
|
| 36 |
+
<template>
|
| 37 |
+
<div ref="contentListRef" class="talk-wrapper">
|
| 38 |
+
<div v-for="node, index in chatContent" :class="[node.type == 'answer' ? 'cont-left' : 'cont-right']" :key="index">
|
| 39 |
+
<div :class="[node.type == 'answer' ? 'text-left' : 'text-right']">
|
| 40 |
+
{{ node.content }}
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<!-- <div class="cont-left">
|
| 45 |
+
<div>
|
| 46 |
+
<a-avatar size="large" :style="{ backgroundColor: 'green', verticalAlign: 'middle' }" :gap="1">
|
| 47 |
+
{{ 'AI' }}
|
| 48 |
+
</a-avatar>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="text-left">
|
| 51 |
+
你好,今天的天气怎么样?
|
| 52 |
+
早上好,今天的天气不错,适合出去走走。
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
<div class="cont-right">
|
| 56 |
+
<div class="text-right">
|
| 57 |
+
是的,今天天气很好,阳光明媚。
|
| 58 |
+
</div>
|
| 59 |
+
<div>
|
| 60 |
+
<a-avatar size="large" :style="{ backgroundColor: 'orange', verticalAlign: 'middle' }" :gap="1">
|
| 61 |
+
{{ 'U' }}
|
| 62 |
+
</a-avatar>
|
| 63 |
+
</div>
|
| 64 |
+
</div> -->
|
| 65 |
+
|
| 66 |
+
</div>
|
| 67 |
+
</template>
|
| 68 |
+
|
| 69 |
+
<style lang="scss" scoped>
|
| 70 |
+
.talk-wrapper {
|
| 71 |
+
width: auto;
|
| 72 |
+
height: calc(100vh - 100px);
|
| 73 |
+
overflow-y: scroll;
|
| 74 |
+
padding: 20px 240px 0 240px;
|
| 75 |
+
display: flex;
|
| 76 |
+
flex-direction: column;
|
| 77 |
+
align-items: flex-start;
|
| 78 |
+
justify-content: flex-start;
|
| 79 |
+
|
| 80 |
+
.cont-left {
|
| 81 |
+
width: 100%;
|
| 82 |
+
|
| 83 |
+
margin: 24px 0;
|
| 84 |
+
display: flex;
|
| 85 |
+
justify-content: flex-start;
|
| 86 |
+
align-items: flex-start;
|
| 87 |
+
.text-left {
|
| 88 |
+
color: #222;
|
| 89 |
+
font-size: 16px;
|
| 90 |
+
font-weight: 400;
|
| 91 |
+
text-align: left;
|
| 92 |
+
line-height: 2;
|
| 93 |
+
margin-left: 12px;
|
| 94 |
+
margin-top: 6px;
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.cont-right {
|
| 99 |
+
width: 100%;
|
| 100 |
+
margin: 24px 0;
|
| 101 |
+
display: flex;
|
| 102 |
+
justify-content: flex-end;
|
| 103 |
+
align-items: flex-start;
|
| 104 |
+
|
| 105 |
+
.text-right {
|
| 106 |
+
color: #444;
|
| 107 |
+
font-size: 16px;
|
| 108 |
+
font-weight: 400;
|
| 109 |
+
text-align: end;
|
| 110 |
+
line-height: 2;
|
| 111 |
+
margin-right: 12px;
|
| 112 |
+
background: #ccc;
|
| 113 |
+
border-radius: 8px;
|
| 114 |
+
border-top-right-radius: 0;
|
| 115 |
+
padding: 8px;
|
| 116 |
+
}
|
| 117 |
+
}
|
| 118 |
+
}
|
| 119 |
+
</style>
|
frontend/src/views/Home/Components/DynamicBall.vue
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
|
| 3 |
+
import router from "@/router.ts";
|
| 4 |
+
import { useSettingsStore } from "@/stores/config.ts";
|
| 5 |
+
import { onMounted, ref, watch, defineProps } from "vue";
|
| 6 |
+
import { Vue3Lottie, AnimationItem } from 'vue3-lottie'
|
| 7 |
+
|
| 8 |
+
import ballJson from "@/assets/ball.json";
|
| 9 |
+
|
| 10 |
+
const props = defineProps({
|
| 11 |
+
isPlaying: {
|
| 12 |
+
type: Boolean,
|
| 13 |
+
default: true
|
| 14 |
+
}
|
| 15 |
+
});
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
onMounted(async () => {
|
| 19 |
+
// console.log('config', settingsStore.$state)
|
| 20 |
+
})
|
| 21 |
+
const chatAction = () => {
|
| 22 |
+
router.replace('/home')
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
const inputType = ref<string>();
|
| 26 |
+
const role = ref<string>();
|
| 27 |
+
const onTypeChange = (e: any) => {
|
| 28 |
+
console.log('onTypeChange', e.target.value)
|
| 29 |
+
// settingsStore.$state.file_type = e.target.value
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const onRoleChange = (e: any) => {
|
| 33 |
+
console.log('onRoleChange', e.target.value)
|
| 34 |
+
// settingsStore.$state.role_name = e.target.value
|
| 35 |
+
// stateStore.changeRole(e.target.value)
|
| 36 |
+
// console.log('role_name', settingsStore.$state.role_name)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const isAnimationPaused = ref<boolean>(true);
|
| 40 |
+
const animationSpeed = ref<number>(0); // 动画播放速度,0 表示静止
|
| 41 |
+
|
| 42 |
+
watch(() => props.isPlaying, (newIsPlayingState) => {
|
| 43 |
+
if (newIsPlayingState) {
|
| 44 |
+
// 如果父组件要求播放
|
| 45 |
+
isAnimationPaused.value = false;
|
| 46 |
+
animationSpeed.value = 1; // 正常速度
|
| 47 |
+
} else {
|
| 48 |
+
// 如果父组件要求暂停
|
| 49 |
+
isAnimationPaused.value = true;
|
| 50 |
+
animationSpeed.value = 0; // 速度为0以暂停
|
| 51 |
+
}
|
| 52 |
+
console.log(`Animation controlled by prop: isPlaying=${newIsPlayingState}, Paused: ${isAnimationPaused.value}, Speed: ${animationSpeed.value}`);
|
| 53 |
+
}, { immediate: true });
|
| 54 |
+
|
| 55 |
+
</script>
|
| 56 |
+
|
| 57 |
+
<template>
|
| 58 |
+
<div class="ball-wrapper">
|
| 59 |
+
<Vue3Lottie :animationData="ballJson" :autoplay="true" :pauseAnimation="isAnimationPaused"
|
| 60 |
+
:speed="animationSpeed" :height="340" :width="340" />
|
| 61 |
+
</div>
|
| 62 |
+
</template>
|
| 63 |
+
|
| 64 |
+
<style lang="scss" scoped>
|
| 65 |
+
.ball-wrapper {
|
| 66 |
+
width: 100%;
|
| 67 |
+
height: calc(100vh - 100px);
|
| 68 |
+
display: flex;
|
| 69 |
+
flex-direction: column;
|
| 70 |
+
align-items: center;
|
| 71 |
+
justify-content: space-around;
|
| 72 |
+
|
| 73 |
+
}
|
| 74 |
+
</style>
|
frontend/src/views/Home/index.vue
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
|
| 3 |
+
import router from "@/router.ts";
|
| 4 |
+
import { useSettingsStore } from "@/stores/config.ts";
|
| 5 |
+
import { onMounted, onUnmounted, ref, reactive } from "vue";
|
| 6 |
+
import { SettingTwoTone } from "@ant-design/icons-vue";
|
| 7 |
+
import DynamicBall from "@/views/Home/Components/DynamicBall.vue";
|
| 8 |
+
import ChatText from "@/views/Home/Components/ChatText.vue";
|
| 9 |
+
import axios from "axios";
|
| 10 |
+
import { test_server } from "@/config/client_config.ts";
|
| 11 |
+
|
| 12 |
+
const base_url = axios.defaults.baseURL
|
| 13 |
+
|
| 14 |
+
const settingsStore = useSettingsStore()
|
| 15 |
+
|
| 16 |
+
import mic_on from "@/assets/microphone.png"
|
| 17 |
+
import mic_off from "@/assets/microphone_off.png"
|
| 18 |
+
import text_on from "@/assets/text.png"
|
| 19 |
+
import text_off from "@/assets/text_off.png"
|
| 20 |
+
import close from "@/assets/close.png"
|
| 21 |
+
import download from "@/assets/download.png"
|
| 22 |
+
|
| 23 |
+
const host = import.meta.env.PROD ? window.location.host : test_server
|
| 24 |
+
|
| 25 |
+
let ws_prefix = 'ws'
|
| 26 |
+
if (host.startsWith('127.0.0.1') || host.startsWith('localhost')) {
|
| 27 |
+
ws_prefix = 'ws'
|
| 28 |
+
} else {
|
| 29 |
+
ws_prefix = 'wss'
|
| 30 |
+
}
|
| 31 |
+
const ws_url = `${ws_prefix}://` + host + `/api/v1/ws`
|
| 32 |
+
console.warn('ws_url: ', ws_url)
|
| 33 |
+
|
| 34 |
+
const sock = ref(null)
|
| 35 |
+
const startWebSock = async () => {
|
| 36 |
+
console.warn('start websocket ...')
|
| 37 |
+
// 确保在创建 WebSocket 连接之前关闭已有连接
|
| 38 |
+
// @ts-ignore
|
| 39 |
+
if (sock.value && sock.value.readyState !== WebSocket.CLOSED) {
|
| 40 |
+
// @ts-ignore
|
| 41 |
+
sock.value.close();
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
// const socket_url = `${ws_url}${lang_str}${'&vad=' + vadValueRef.value}`
|
| 45 |
+
const socket_url = `${ws_url}`
|
| 46 |
+
// @ts-ignore
|
| 47 |
+
sock.value = new WebSocket(socket_url)
|
| 48 |
+
// @ts-ignore
|
| 49 |
+
sock.value.binaryType = "arraybuffer";
|
| 50 |
+
console.warn('created web socket ...')
|
| 51 |
+
// @ts-ignore
|
| 52 |
+
sock.value.addEventListener('open', () => {
|
| 53 |
+
console.log('WebSocket 连接成功');
|
| 54 |
+
});
|
| 55 |
+
// @ts-ignore
|
| 56 |
+
sock.value.addEventListener('close', () => {
|
| 57 |
+
console.log('WebSocket 连接已关闭');
|
| 58 |
+
});
|
| 59 |
+
// @ts-ignore
|
| 60 |
+
sock.value.onclose = (event: any) => {
|
| 61 |
+
console.log('code:', event.code, 'reason:', event.reason, 'wasClean:', event.wasClean)
|
| 62 |
+
// https://www.cnblogs.com/gxp69/p/11736749.html
|
| 63 |
+
console.log('WebSocket 连接已关闭:', event);
|
| 64 |
+
};
|
| 65 |
+
// @ts-ignore
|
| 66 |
+
sock.value.addEventListener('error', (error) => {
|
| 67 |
+
console.error('WebSocket 连接错误:', error);
|
| 68 |
+
});
|
| 69 |
+
// @ts-ignore
|
| 70 |
+
sock.value.addEventListener('message', (event) => {
|
| 71 |
+
try { // 添加 try-catch 保证 JSON 解析失败不中断程序
|
| 72 |
+
const data = JSON.parse(event.data)
|
| 73 |
+
console.log('WebSocket 收到消息:', data);
|
| 74 |
+
if (data) {
|
| 75 |
+
updateViewData(data)
|
| 76 |
+
}
|
| 77 |
+
} catch (e) {
|
| 78 |
+
console.error("解析 WebSocket 消息失败:", e, "原始数据:", event.data);
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
const stopWebSock = async () => {
|
| 85 |
+
if (sock.value) {
|
| 86 |
+
console.log("主动关闭 WebSocket 连接");
|
| 87 |
+
// @ts-ignore
|
| 88 |
+
sock.value.close(1000, "User closed connection"); // 使用标准关闭代码
|
| 89 |
+
sock.value = null;
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
// @ts-ignore
|
| 94 |
+
const currentSession: any = reactive([]);
|
| 95 |
+
|
| 96 |
+
const updateViewData = (data: any) => {
|
| 97 |
+
// console.log('updateViewData: ', data)
|
| 98 |
+
/*
|
| 99 |
+
data = {
|
| 100 |
+
"message_type": "question",
|
| 101 |
+
"session_id": "ae758c85-26ae-4323-9ca2-7a158bfd9a13",
|
| 102 |
+
"task_id": "287ca258-9e62-44da-b768-a44c7c339922",
|
| 103 |
+
"question": "你好。"
|
| 104 |
+
}
|
| 105 |
+
data = {
|
| 106 |
+
"message_type": "answer",
|
| 107 |
+
"session_id": "ae758c85-26ae-4323-9ca2-7a158bfd9a13",
|
| 108 |
+
"task_id": "287ca258-9e62-44da-b768-a44c7c339922",
|
| 109 |
+
"answer_index": 0,
|
| 110 |
+
"answer": "你好,"
|
| 111 |
+
}
|
| 112 |
+
*/
|
| 113 |
+
if (data) {
|
| 114 |
+
if (data['message_type'] === 'question') {
|
| 115 |
+
// 添加问题到当前会话
|
| 116 |
+
if (data.question && data.question.length > 0) {
|
| 117 |
+
|
| 118 |
+
// 如果前一个node是问题,且task_id相同,则更新问题内容
|
| 119 |
+
if (currentSession.length > 0 &&
|
| 120 |
+
currentSession[currentSession.length - 1].type === 'question' &&
|
| 121 |
+
currentSession[currentSession.length - 1].task_id === data.task_id) {
|
| 122 |
+
currentSession[currentSession.length - 1].content = data.question; // 更新问题内容
|
| 123 |
+
} else {
|
| 124 |
+
// 否则,添加新的问题
|
| 125 |
+
currentSession.push({
|
| 126 |
+
type: 'question',
|
| 127 |
+
content: data.question,
|
| 128 |
+
task_id: data.task_id
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
} else if (data['message_type'] === 'answer') {
|
| 134 |
+
if (data.answer_index === 0) {
|
| 135 |
+
currentSession.push({
|
| 136 |
+
type: 'answer',
|
| 137 |
+
content: data.answer,
|
| 138 |
+
task_id: data.task_id,
|
| 139 |
+
});
|
| 140 |
+
} else {
|
| 141 |
+
// 如果是后续的答案,更新当前会话中的答案
|
| 142 |
+
const lastAnswer = currentSession[currentSession.length - 1];
|
| 143 |
+
if (lastAnswer &&
|
| 144 |
+
lastAnswer.task_id === data.task_id &&
|
| 145 |
+
lastAnswer.content !== null && lastAnswer.content.length > 0) {
|
| 146 |
+
if (lastAnswer.type === 'answer') {
|
| 147 |
+
lastAnswer.content += data.answer; // 累加答案内容
|
| 148 |
+
} else {
|
| 149 |
+
console.warn('Received answer without a matching question in the current session');
|
| 150 |
+
currentSession.push({
|
| 151 |
+
type: data.type,
|
| 152 |
+
content: data.answer,
|
| 153 |
+
task_id: data.task_id,
|
| 154 |
+
});
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
} else {
|
| 158 |
+
console.warn('Received answer without a matching question in the current session');
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
}
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
onMounted( async () => {
|
| 167 |
+
// console.log('config', settingsStore.$state)
|
| 168 |
+
await startWebSock();
|
| 169 |
+
})
|
| 170 |
+
|
| 171 |
+
onUnmounted( async () => {
|
| 172 |
+
console.warn('onUnmounted, stop web socket ...')
|
| 173 |
+
stopWebSock();
|
| 174 |
+
});
|
| 175 |
+
|
| 176 |
+
const backAction = async () => {
|
| 177 |
+
const state = await stopAudioCapture();
|
| 178 |
+
if (!state) {
|
| 179 |
+
console.error('Failed to stop audio chat system service');
|
| 180 |
+
return;
|
| 181 |
+
}
|
| 182 |
+
router.replace('/')
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
const stopActionLoading = ref<boolean>(false);
|
| 186 |
+
const stopAudioCapture = async () => {
|
| 187 |
+
try {
|
| 188 |
+
stopActionLoading.value = true;
|
| 189 |
+
|
| 190 |
+
const response = await fetch(`${base_url}/system/stop`, {
|
| 191 |
+
method: 'POST',
|
| 192 |
+
headers: {
|
| 193 |
+
'Content-Type': 'application/json',
|
| 194 |
+
},
|
| 195 |
+
body: null
|
| 196 |
+
});
|
| 197 |
+
if (!response.ok) {
|
| 198 |
+
|
| 199 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 200 |
+
}
|
| 201 |
+
const data = await response.json();
|
| 202 |
+
console.log('ASR Instance stopped successfully:', data);
|
| 203 |
+
return true;
|
| 204 |
+
} catch (error) {
|
| 205 |
+
console.error('Error stop record :', error);
|
| 206 |
+
return false;
|
| 207 |
+
} finally {
|
| 208 |
+
stopActionLoading.value = false;
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
const micActionLoading = ref<boolean>(false);
|
| 213 |
+
const pauseAudioCapture = async () => {
|
| 214 |
+
try {
|
| 215 |
+
micActionLoading.value = true;
|
| 216 |
+
const response = await fetch(`${base_url}/system/pause`, {
|
| 217 |
+
method: 'POST',
|
| 218 |
+
headers: {
|
| 219 |
+
'Content-Type': 'application/json',
|
| 220 |
+
},
|
| 221 |
+
body: null
|
| 222 |
+
});
|
| 223 |
+
if (!response.ok) {
|
| 224 |
+
|
| 225 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 226 |
+
}
|
| 227 |
+
const data = await response.json();
|
| 228 |
+
console.log('Mic paused successfully:', data);
|
| 229 |
+
return true;
|
| 230 |
+
} catch (error) {
|
| 231 |
+
console.error('Error pause mic:', error);
|
| 232 |
+
return false;
|
| 233 |
+
} finally {
|
| 234 |
+
micActionLoading.value = false;
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
const resumeAudioCapture = async () => {
|
| 239 |
+
try {
|
| 240 |
+
micActionLoading.value = true;
|
| 241 |
+
const response = await fetch(`${base_url}/system/resume`, {
|
| 242 |
+
method: 'POST',
|
| 243 |
+
headers: {
|
| 244 |
+
'Content-Type': 'application/json',
|
| 245 |
+
},
|
| 246 |
+
body: null
|
| 247 |
+
});
|
| 248 |
+
if (!response.ok) {
|
| 249 |
+
|
| 250 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 251 |
+
}
|
| 252 |
+
const data = await response.json();
|
| 253 |
+
console.log('Mic resume successfully:', data);
|
| 254 |
+
return true;
|
| 255 |
+
} catch (error) {
|
| 256 |
+
console.error('Error resume mic:', error);
|
| 257 |
+
return false;
|
| 258 |
+
} finally {
|
| 259 |
+
micActionLoading.value = false;
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
const mic_working = ref<boolean>(true);
|
| 265 |
+
const toggleMic = async () => {
|
| 266 |
+
if (mic_working.value) {
|
| 267 |
+
const state = await pauseAudioCapture();
|
| 268 |
+
if (!state) {
|
| 269 |
+
console.error('Failed to stop audio chat system service');
|
| 270 |
+
return;
|
| 271 |
+
}
|
| 272 |
+
} else {
|
| 273 |
+
const state = await resumeAudioCapture();
|
| 274 |
+
if (!state) {
|
| 275 |
+
console.error('Failed to start audio chat system service');
|
| 276 |
+
return;
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
mic_working.value = !mic_working.value;
|
| 280 |
+
console.log('mic_state', mic_working.value);
|
| 281 |
+
};
|
| 282 |
+
const text_state = ref<boolean>(false);
|
| 283 |
+
const toggleText = () => {
|
| 284 |
+
text_state.value = !text_state.value;
|
| 285 |
+
console.log('text_state', text_state.value);
|
| 286 |
+
};
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
</script>
|
| 290 |
+
|
| 291 |
+
<template>
|
| 292 |
+
<div class="chat-wrapper">
|
| 293 |
+
<div class="content">
|
| 294 |
+
<div v-if="!text_state">
|
| 295 |
+
<DynamicBall :isPlaying="mic_working" />
|
| 296 |
+
</div>
|
| 297 |
+
<div v-if="text_state">
|
| 298 |
+
<ChatText :chatContent="currentSession" :isPlaying="mic_working" />
|
| 299 |
+
</div>
|
| 300 |
+
</div>
|
| 301 |
+
|
| 302 |
+
<div class="actions">
|
| 303 |
+
<div class="holder">
|
| 304 |
+
<span> </span>
|
| 305 |
+
</div>
|
| 306 |
+
<div class="btns">
|
| 307 |
+
<a-button type="text" style="width:64px; height: 64px;" :loading="micActionLoading" @click="toggleMic">
|
| 308 |
+
<template #icon>
|
| 309 |
+
<img :src="mic_working == true ? mic_on : mic_off" width="50" height="50" alt="mic_on" />
|
| 310 |
+
</template>
|
| 311 |
+
</a-button>
|
| 312 |
+
<a-button type="text" style="width:64px; height: 64px;" @click="toggleText">
|
| 313 |
+
<template #icon>
|
| 314 |
+
<img :src="text_state == true ? text_on : text_off" width="50" height="50" alt="text_off" />
|
| 315 |
+
</template>
|
| 316 |
+
</a-button>
|
| 317 |
+
<a-button type="text" style="width:64px; height: 64px;" :loading="stopActionLoading" @click="backAction">
|
| 318 |
+
<template #icon>
|
| 319 |
+
<img :src="close" width="50" height="50" alt="close" />
|
| 320 |
+
</template>
|
| 321 |
+
</a-button>
|
| 322 |
+
</div>
|
| 323 |
+
<div class="download-wrapper">
|
| 324 |
+
<!-- <a-button type="text" style="width:34px; height: 34px;">
|
| 325 |
+
<template #icon>
|
| 326 |
+
<img :src="download" width="20" height="20" alt="settings" />
|
| 327 |
+
</template>
|
| 328 |
+
</a-button> -->
|
| 329 |
+
</div>
|
| 330 |
+
</div>
|
| 331 |
+
</div>
|
| 332 |
+
</template>
|
| 333 |
+
|
| 334 |
+
<style lang="scss" scoped>
|
| 335 |
+
.chat-wrapper {
|
| 336 |
+
width: 100%;
|
| 337 |
+
height: 100%;
|
| 338 |
+
background-image: url('@/assets/bg.png');
|
| 339 |
+
background-repeat: no-repeat;
|
| 340 |
+
background-attachment: fixed;
|
| 341 |
+
background-size: cover;
|
| 342 |
+
background-position: center;
|
| 343 |
+
display: flex;
|
| 344 |
+
flex-direction: column;
|
| 345 |
+
align-items: center;
|
| 346 |
+
justify-content: space-between;
|
| 347 |
+
color: #fff;
|
| 348 |
+
|
| 349 |
+
.content {
|
| 350 |
+
width: 100%;
|
| 351 |
+
height: auto;
|
| 352 |
+
display: flex;
|
| 353 |
+
flex-direction: column;
|
| 354 |
+
justify-content: space-around;
|
| 355 |
+
|
| 356 |
+
.inner-content {
|
| 357 |
+
display: flex;
|
| 358 |
+
flex-direction: column;
|
| 359 |
+
align-items: center;
|
| 360 |
+
justify-content: center;
|
| 361 |
+
text-align: center;
|
| 362 |
+
padding: 20px;
|
| 363 |
+
|
| 364 |
+
.text-box {
|
| 365 |
+
color: #000;
|
| 366 |
+
margin-bottom: 36px;
|
| 367 |
+
|
| 368 |
+
.title {
|
| 369 |
+
font-size: 24px;
|
| 370 |
+
font-weight: 600;
|
| 371 |
+
margin-bottom: 24px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
.sub-title {
|
| 375 |
+
font-size: 15px;
|
| 376 |
+
margin-top: 10px;
|
| 377 |
+
}
|
| 378 |
+
}
|
| 379 |
+
|
| 380 |
+
.btn-box {
|
| 381 |
+
width: 224px;
|
| 382 |
+
height: 80px;
|
| 383 |
+
}
|
| 384 |
+
}
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
.actions {
|
| 388 |
+
width: 100%;
|
| 389 |
+
height: 100px;
|
| 390 |
+
|
| 391 |
+
display: flex;
|
| 392 |
+
justify-content: space-between;
|
| 393 |
+
align-items: center;
|
| 394 |
+
|
| 395 |
+
.holder {
|
| 396 |
+
width: 64px;
|
| 397 |
+
height: 48px;
|
| 398 |
+
}
|
| 399 |
+
.btns {
|
| 400 |
+
width: 450px;
|
| 401 |
+
height: 96px;
|
| 402 |
+
display: flex;
|
| 403 |
+
justify-content: space-around;
|
| 404 |
+
align-items: flex-start;
|
| 405 |
+
}
|
| 406 |
+
.download-wrapper {
|
| 407 |
+
width: 64px;
|
| 408 |
+
height: 64px;
|
| 409 |
+
display: flex;
|
| 410 |
+
justify-content: flex-start;
|
| 411 |
+
align-items: center;
|
| 412 |
+
margin-right: 0;
|
| 413 |
+
|
| 414 |
+
img {
|
| 415 |
+
width: 24px;
|
| 416 |
+
height: 24px;
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
}
|
| 421 |
+
</style>
|
frontend/src/views/Home/index2.vue
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
import router from "@/router.ts";
|
| 3 |
+
import {
|
| 4 |
+
DownloadOutlined, FileMarkdownOutlined, SettingOutlined,
|
| 5 |
+
AudioOutlined, DeleteOutlined, FileTextOutlined,
|
| 6 |
+
ArrowRightOutlined, ContainerOutlined, UploadOutlined
|
| 7 |
+
} from "@ant-design/icons-vue";
|
| 8 |
+
import { test_server } from "@/config/client_config.ts";
|
| 9 |
+
import axios from "axios";
|
| 10 |
+
import { nextTick, onMounted, onUnmounted, reactive, ref, createVNode, computed, watch } from "vue";
|
| 11 |
+
|
| 12 |
+
import { useSessionStore, downloadSessionData } from "@/stores/session";
|
| 13 |
+
import type { SessionSummary, SessionNode } from '@/stores/session'; // 导入类型
|
| 14 |
+
import { useSettingsStore } from "@/stores/config.ts";
|
| 15 |
+
const sessionStore = useSessionStore()
|
| 16 |
+
const settingsStore = useSettingsStore();
|
| 17 |
+
// https://github.surmon.me/videojs-player video.js debug page;
|
| 18 |
+
// const base_url = 'http://192.168.110.102:8000'
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
const host = import.meta.env.PROD ? window.location.host : test_server
|
| 22 |
+
// const pathname = window.location.pathname
|
| 23 |
+
let ws_prefix = 'ws'
|
| 24 |
+
if (host.startsWith('127.0.0.1') || host.startsWith('localhost')) {
|
| 25 |
+
ws_prefix = 'ws'
|
| 26 |
+
} else {
|
| 27 |
+
ws_prefix = 'wss'
|
| 28 |
+
}
|
| 29 |
+
const ws_url = `${ws_prefix}://` + host + `/ws?`
|
| 30 |
+
console.warn('ws_url: ', ws_url)
|
| 31 |
+
|
| 32 |
+
const sock = ref(null)
|
| 33 |
+
const startWebSock = async (lang_str: string) => {
|
| 34 |
+
console.warn('start websocket ...')
|
| 35 |
+
// 确保在创建 WebSocket 连接之前关闭已有连接
|
| 36 |
+
// @ts-ignore
|
| 37 |
+
if (sock.value && sock.value.readyState !== WebSocket.CLOSED) {
|
| 38 |
+
// @ts-ignore
|
| 39 |
+
sock.value.close();
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
// const socket_url = `${ws_url}${lang_str}${'&vad=' + vadValueRef.value}`
|
| 43 |
+
const socket_url = `${ws_url}${lang_str}`
|
| 44 |
+
// @ts-ignore
|
| 45 |
+
sock.value = new WebSocket(socket_url)
|
| 46 |
+
// @ts-ignore
|
| 47 |
+
sock.value.binaryType = "arraybuffer";
|
| 48 |
+
console.warn('created web socket ...')
|
| 49 |
+
// @ts-ignore
|
| 50 |
+
sock.value.addEventListener('open', () => {
|
| 51 |
+
console.log('WebSocket 连接成功');
|
| 52 |
+
startAudioCapture(); // WebSocket 连接成功后开始音频捕获
|
| 53 |
+
isRecording.value = true; // 连接成功时自动开始录音
|
| 54 |
+
});
|
| 55 |
+
// @ts-ignore
|
| 56 |
+
sock.value.addEventListener('close', () => {
|
| 57 |
+
console.log('WebSocket 连接已关闭');
|
| 58 |
+
});
|
| 59 |
+
// @ts-ignore
|
| 60 |
+
sock.value.onclose = (event: any) => {
|
| 61 |
+
console.log('code:', event.code, 'reason:', event.reason, 'wasClean:', event.wasClean)
|
| 62 |
+
// https://www.cnblogs.com/gxp69/p/11736749.html
|
| 63 |
+
console.log('WebSocket 连接已关闭:', event);
|
| 64 |
+
};
|
| 65 |
+
// @ts-ignore
|
| 66 |
+
sock.value.addEventListener('error', (error) => {
|
| 67 |
+
console.error('WebSocket 连接错误:', error);
|
| 68 |
+
});
|
| 69 |
+
// @ts-ignore
|
| 70 |
+
sock.value.addEventListener('message', (event) => {
|
| 71 |
+
try { // 添加 try-catch 保证 JSON 解析失败不中断程序
|
| 72 |
+
const data = JSON.parse(event.data)
|
| 73 |
+
console.log('WebSocket 收到消息:', data);
|
| 74 |
+
if (data && data['result']) {
|
| 75 |
+
updateViewData(data['result'])
|
| 76 |
+
}
|
| 77 |
+
} catch (e) {
|
| 78 |
+
console.error("解析 WebSocket 消息失败:", e, "原始数据:", event.data);
|
| 79 |
+
}
|
| 80 |
+
});
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
const stopWebSock = async () => {
|
| 84 |
+
if (sock.value) {
|
| 85 |
+
console.log("主动关闭 WebSocket 连接");
|
| 86 |
+
// @ts-ignore
|
| 87 |
+
sock.value.close(1000, "User closed connection"); // 使用标准关闭代码
|
| 88 |
+
sock.value = null;
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const audioStreamRef = ref<MediaStream | null>(null);
|
| 93 |
+
const recorderRef = ref(null);
|
| 94 |
+
const audioContextRef = ref<AudioContext | null>(null);
|
| 95 |
+
const sourceNodeRef = ref(null)
|
| 96 |
+
const processorNodeRef = ref(null)
|
| 97 |
+
const isRecording = ref<boolean>(false);
|
| 98 |
+
|
| 99 |
+
// 启动音频捕获
|
| 100 |
+
const startAudioCapture = async () => {
|
| 101 |
+
try {
|
| 102 |
+
// 检查 AudioContext 是否支持
|
| 103 |
+
// @ts-ignore
|
| 104 |
+
if (!window.AudioContext && !window.webkitAudioContext) {
|
| 105 |
+
alert("浏览器不支持 Web Audio API");
|
| 106 |
+
throw new Error("浏览器不支持 Web Audio API");
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const stream = await navigator.mediaDevices.getUserMedia({
|
| 110 |
+
audio: {
|
| 111 |
+
// @ts-ignore
|
| 112 |
+
sampleRate: 16000,
|
| 113 |
+
channelCount: 1,
|
| 114 |
+
// echoCancellation: true,
|
| 115 |
+
// noiseSuppression: true,
|
| 116 |
+
// autoGainControl: true,
|
| 117 |
+
},
|
| 118 |
+
});
|
| 119 |
+
audioStreamRef.value = stream;
|
| 120 |
+
|
| 121 |
+
// 创建 AudioContext,指定采样率为 16kHz
|
| 122 |
+
const audioContext = new AudioContext({ sampleRate: 16000 });
|
| 123 |
+
audioContextRef.value = audioContext;
|
| 124 |
+
|
| 125 |
+
// 创建媒体流源节点
|
| 126 |
+
const source = audioContext.createMediaStreamSource(stream);
|
| 127 |
+
// @ts-ignore
|
| 128 |
+
sourceNodeRef.value = source;
|
| 129 |
+
|
| 130 |
+
// 创建脚本处理器节点
|
| 131 |
+
const processor = audioContext.createScriptProcessor(4096, 1, 1);
|
| 132 |
+
// @ts-ignore
|
| 133 |
+
processorNodeRef.value = processor;
|
| 134 |
+
|
| 135 |
+
// 连接节点
|
| 136 |
+
source.connect(processor);
|
| 137 |
+
processor.connect(audioContext.destination);
|
| 138 |
+
|
| 139 |
+
// 设置音频处理回调
|
| 140 |
+
processor.onaudioprocess = (e: AudioProcessingEvent) => {
|
| 141 |
+
// @ts-ignore
|
| 142 |
+
if (!isRecording.value || !sock.value || sock.value.readyState !== WebSocket.OPEN) return;
|
| 143 |
+
|
| 144 |
+
const input = e.inputBuffer.getChannelData(0);
|
| 145 |
+
const buffer = new Int16Array(input.length);
|
| 146 |
+
|
| 147 |
+
for (let i = 0; i < input.length; i++) {
|
| 148 |
+
buffer[i] = Math.max(-1, Math.min(1, input[i])) * 0x7FFF;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
sendAudioChunk(buffer);
|
| 152 |
+
};
|
| 153 |
+
|
| 154 |
+
isRecording.value = true;
|
| 155 |
+
console.log('音频捕获已启动');
|
| 156 |
+
|
| 157 |
+
} catch (err) {
|
| 158 |
+
console.error('音频捕获失败:', err);
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
const sendAudioChunk = (audioBuffer: any) => {
|
| 163 |
+
// @ts-ignore
|
| 164 |
+
if (sock.value && sock.value.readyState === WebSocket.OPEN) {
|
| 165 |
+
// @ts-ignore
|
| 166 |
+
sock.value.send(audioBuffer);
|
| 167 |
+
// console.log('WebSocket send audio chunk success'); // 减少日志量
|
| 168 |
+
} else {
|
| 169 |
+
console.error('WebSocket 未连接或未打开,无法发送音频');
|
| 170 |
+
// 考虑停止录音
|
| 171 |
+
// handleRecordingSwitch(false);
|
| 172 |
+
}
|
| 173 |
+
};
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
// 请求录音权限并开始录音
|
| 177 |
+
const requirePermissionAction = async () => {
|
| 178 |
+
console.log('requirePermissionAction');
|
| 179 |
+
try {
|
| 180 |
+
// 确保 WebSocket 连接已建立
|
| 181 |
+
// @ts-ignore
|
| 182 |
+
if (!sock.value || sock.value.readyState !== WebSocket.OPEN) {
|
| 183 |
+
console.log('current lang_str : ', transLanguageValue.value)
|
| 184 |
+
const lang_str = transLanguageValue.value
|
| 185 |
+
|
| 186 |
+
resetViewData();
|
| 187 |
+
await startWebSock(lang_str);
|
| 188 |
+
} else {
|
| 189 |
+
// 如果 WebSocket 已经连接,可能只需要重新开始音频捕获(如果之前停止了)
|
| 190 |
+
if (!audioStreamRef.value) {
|
| 191 |
+
await startAudioCapture();
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
} catch (e: any) {
|
| 195 |
+
isRecording.value = false; // 确保出错时关闭开关
|
| 196 |
+
console.log('Error accessing microphone: ', e);
|
| 197 |
+
}
|
| 198 |
+
};
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
// 格式化时间戳函数
|
| 202 |
+
const formatTimestamp = (ms: number): string => {
|
| 203 |
+
const date = new Date(ms);
|
| 204 |
+
const year = date.getFullYear();
|
| 205 |
+
const month = String(date.getMonth() + 1).padStart(2, '0'); // 月份从 0 开始,需要 +1
|
| 206 |
+
const day = String(date.getDate()).padStart(2, '0');
|
| 207 |
+
const hours = String(date.getHours()).padStart(2, '0');
|
| 208 |
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
| 209 |
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
| 210 |
+
|
| 211 |
+
return `${year}-${month}-${day}-${hours}:${minutes}:${seconds}`;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
onMounted(() => {
|
| 215 |
+
console.log('[translator]: mounted')
|
| 216 |
+
fontSizeRef.value = settingsStore.$state.fs
|
| 217 |
+
maxWidthRef.value = settingsStore.$state.width_max
|
| 218 |
+
vadValueRef.value = settingsStore.$state.vad
|
| 219 |
+
|
| 220 |
+
if (sessionStore.isSessionActive) {
|
| 221 |
+
console.warn("检测到上次会话未正常结束,重置状态。");
|
| 222 |
+
sessionStore.$reset(); // 或者手动重置相关状态
|
| 223 |
+
// isRecording.value = false; // 确保 UI 同步
|
| 224 |
+
}
|
| 225 |
+
})
|
| 226 |
+
|
| 227 |
+
onUnmounted(() => {
|
| 228 |
+
console.log('[HomePage]: unmounted')
|
| 229 |
+
if (sock.value) {
|
| 230 |
+
// @ts-ignore
|
| 231 |
+
sock.value.close();
|
| 232 |
+
}
|
| 233 |
+
if (recorderRef.value) {
|
| 234 |
+
stopRecording();
|
| 235 |
+
sessionStore.endSession(); // 结束当前会话
|
| 236 |
+
}
|
| 237 |
+
})
|
| 238 |
+
|
| 239 |
+
// 停止录音
|
| 240 |
+
const stopRecording = () => {
|
| 241 |
+
isRecording.value = false;
|
| 242 |
+
|
| 243 |
+
stopWebSock();
|
| 244 |
+
console.log('音频捕获已停止');
|
| 245 |
+
|
| 246 |
+
if (processorNodeRef.value) {
|
| 247 |
+
// @ts-ignore
|
| 248 |
+
processorNodeRef.value.disconnect();
|
| 249 |
+
processorNodeRef.value = null;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
if (sourceNodeRef.value) {
|
| 253 |
+
// @ts-ignore
|
| 254 |
+
sourceNodeRef.value.disconnect();
|
| 255 |
+
sourceNodeRef.value = null;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
if (audioStreamRef.value) {
|
| 259 |
+
audioStreamRef.value.getTracks().forEach(track => track.stop());
|
| 260 |
+
audioStreamRef.value = null;
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
if (audioContextRef.value) {
|
| 264 |
+
// @ts-ignore
|
| 265 |
+
audioContextRef.value.close();
|
| 266 |
+
audioContextRef.value = null;
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// current_node_text.value = "";
|
| 270 |
+
// current_node_trans_text.value = "";
|
| 271 |
+
// current_node_seg_id.value = "";
|
| 272 |
+
|
| 273 |
+
// 添加当前节点到会话记录;
|
| 274 |
+
const finalNode: SessionNode = {
|
| 275 |
+
id: current_node_seg_id.value || crypto.randomUUID(), // 使用后端 ID 或生成一个
|
| 276 |
+
text: current_node_text.value,
|
| 277 |
+
translatedText: current_node_trans_text.value,
|
| 278 |
+
timestamp: Date.now(),
|
| 279 |
+
};
|
| 280 |
+
sessionStore.addNode(finalNode);
|
| 281 |
+
|
| 282 |
+
console.log('录音已停止');
|
| 283 |
+
};
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
const handleLanguageChange = async (value: string) => {
|
| 287 |
+
console.log(`selected ${value}`);
|
| 288 |
+
isRecording.value = false;
|
| 289 |
+
await stopRecording();
|
| 290 |
+
sessionStore.endSession();
|
| 291 |
+
|
| 292 |
+
console.log('new lang_str: ', value)
|
| 293 |
+
console.log('trans_lang : ', transLanguageValue.value)
|
| 294 |
+
// requirePermissionAction();
|
| 295 |
+
};
|
| 296 |
+
|
| 297 |
+
|
| 298 |
+
const handleRecordingSwitch = (checked: boolean) => {
|
| 299 |
+
isRecording.value = checked;
|
| 300 |
+
if (checked) {
|
| 301 |
+
isRecording.value = true; // 更新UI状态
|
| 302 |
+
requirePermissionAction();
|
| 303 |
+
sessionStore.startSession(); // 开始新的会话
|
| 304 |
+
} else {
|
| 305 |
+
isRecording.value = false; // 更新UI状态
|
| 306 |
+
stopRecording();
|
| 307 |
+
sessionStore.endSession();
|
| 308 |
+
}
|
| 309 |
+
};
|
| 310 |
+
|
| 311 |
+
|
| 312 |
+
const transLanguageValue = ref("from=en&to=zh");
|
| 313 |
+
const options = [
|
| 314 |
+
{ value: "from=en&to=zh", label: "English -> Chinese" },
|
| 315 |
+
{ value: "from=zh&to=en", label: "Chinese -> English" },
|
| 316 |
+
];
|
| 317 |
+
|
| 318 |
+
// @ts-ignore
|
| 319 |
+
const completedNodesForDisplay: any = reactive([])
|
| 320 |
+
|
| 321 |
+
const current_node_text = ref("");
|
| 322 |
+
const current_node_trans_text = ref("");
|
| 323 |
+
const current_node_seg_id = ref("");
|
| 324 |
+
|
| 325 |
+
const updateViewData = (data: any) => {
|
| 326 |
+
console.log('updateViewData: ', data)
|
| 327 |
+
if (data) {
|
| 328 |
+
const { context, from, to, seg_id, partial, tranContent } = data;
|
| 329 |
+
|
| 330 |
+
if (partial == true) {
|
| 331 |
+
current_node_text.value = context;
|
| 332 |
+
current_node_trans_text.value = tranContent;
|
| 333 |
+
current_node_seg_id.value = seg_id;
|
| 334 |
+
|
| 335 |
+
return;
|
| 336 |
+
} else {
|
| 337 |
+
// partial == false,表示一句话结束
|
| 338 |
+
const finalNode: SessionNode = {
|
| 339 |
+
id: seg_id || crypto.randomUUID(), // 使用后端 ID 或生成一个
|
| 340 |
+
text: context,
|
| 341 |
+
translatedText: tranContent,
|
| 342 |
+
timestamp: Date.now(),
|
| 343 |
+
};
|
| 344 |
+
|
| 345 |
+
sessionStore.addNode(finalNode);
|
| 346 |
+
|
| 347 |
+
if (completedNodesForDisplay.length > 100) {
|
| 348 |
+
// 控制显示列表长度,避免 DOM 过多
|
| 349 |
+
completedNodesForDisplay.splice(0, 40);
|
| 350 |
+
}
|
| 351 |
+
completedNodesForDisplay.push(finalNode);
|
| 352 |
+
current_node_text.value = "";
|
| 353 |
+
current_node_trans_text.value = "";
|
| 354 |
+
current_node_seg_id.value = "";
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
scrollToBottom();
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
const resetViewData = () => {
|
| 362 |
+
completedNodesForDisplay.splice(0, completedNodesForDisplay.length); // 清空显示列表
|
| 363 |
+
current_node_text.value = "";
|
| 364 |
+
current_node_trans_text.value = "";
|
| 365 |
+
current_node_seg_id.value = "";
|
| 366 |
+
}
|
| 367 |
+
|
| 368 |
+
const downloadText2 = async (sid: string, nodes: SessionNode[]) => {
|
| 369 |
+
// @ts-ignore
|
| 370 |
+
// 将数组中的每个字符串元素用换行符连接起来
|
| 371 |
+
// const textContent = all_nodes.join('\n');
|
| 372 |
+
const textContent = nodes.map((node: any) => {
|
| 373 |
+
return `[src]: ${node.text}\n${"-".repeat(80)}\n[dst]: ${node.translatedText}\n\n`
|
| 374 |
+
}).join('\n');
|
| 375 |
+
// 创建 Blob 时指定 MIME 类型为 text/plain
|
| 376 |
+
const blob = new Blob([textContent], { type: "text/plain" });
|
| 377 |
+
const url = URL.createObjectURL(blob);
|
| 378 |
+
const a = document.createElement("a");
|
| 379 |
+
a.href = url;
|
| 380 |
+
// 修改下载文件名为 .txt
|
| 381 |
+
a.download = `${sid}.txt`;
|
| 382 |
+
document.body.appendChild(a);
|
| 383 |
+
a.click();
|
| 384 |
+
document.body.removeChild(a);
|
| 385 |
+
URL.revokeObjectURL(url);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
const downloadText = (sid: number, nodes: SessionNode[]) => { // Removed async as it's not needed here
|
| 390 |
+
try {
|
| 391 |
+
if (!nodes || nodes.length === 0) {
|
| 392 |
+
console.warn("No nodes provided for download.");
|
| 393 |
+
alert("No content available to download for this session.");
|
| 394 |
+
return;
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
const textContent = nodes.map((node: SessionNode) => { // Use correct type SessionNode
|
| 398 |
+
// Safely access properties, provide fallback for undefined translatedText
|
| 399 |
+
const srcText = node.text || '(No original text)';
|
| 400 |
+
const dstText = node.translatedText || '(No translation)';
|
| 401 |
+
return `[src]: ${srcText}\n${"-".repeat(80)}\n[dst]: ${dstText}\n\n`;
|
| 402 |
+
}).join(''); // Join without extra newline, as \n\n is already added
|
| 403 |
+
|
| 404 |
+
if (!textContent.trim()) {
|
| 405 |
+
console.warn("Generated text content is empty.");
|
| 406 |
+
alert("Generated content is empty, cannot download.");
|
| 407 |
+
return;
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Create Blob with UTF-8 charset
|
| 411 |
+
const blob = new Blob([textContent], { type: "text/plain;charset=utf-8;" });
|
| 412 |
+
const url = URL.createObjectURL(blob);
|
| 413 |
+
const a = document.createElement("a");
|
| 414 |
+
|
| 415 |
+
a.href = url;
|
| 416 |
+
a.download = `${formatTimestamp(sid)}.txt`; // Ensure filename is set
|
| 417 |
+
a.style.display = 'none'; // Hide the element
|
| 418 |
+
|
| 419 |
+
document.body.appendChild(a);
|
| 420 |
+
console.log(`Attempting to click download link for ${sid}.txt`);
|
| 421 |
+
a.click(); // Trigger the download
|
| 422 |
+
|
| 423 |
+
// Delay cleanup to ensure download initiation
|
| 424 |
+
setTimeout(() => {
|
| 425 |
+
try {
|
| 426 |
+
document.body.removeChild(a);
|
| 427 |
+
URL.revokeObjectURL(url);
|
| 428 |
+
console.log(`Cleaned up resources for ${sid}.txt`);
|
| 429 |
+
} catch (cleanupError) {
|
| 430 |
+
console.error("Error during download cleanup:", cleanupError);
|
| 431 |
+
}
|
| 432 |
+
}, 100); // Delay cleanup by 100ms
|
| 433 |
+
|
| 434 |
+
} catch (error) {
|
| 435 |
+
console.error("Error creating download file:", error);
|
| 436 |
+
alert("An error occurred while preparing the download.");
|
| 437 |
+
}
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
const placeholder_zh = "体验前请检查麦克风是否可用,指定音频语言、译文语言,点击开关按钮开始录音,即可实时获取识别及翻译的文字。"
|
| 442 |
+
const placeholder_en = "Please check if the microphone is available before the experience, specify the audio language and translation language, click the switch button to start recording, and you can get the recognized and translated text in real time."
|
| 443 |
+
|
| 444 |
+
|
| 445 |
+
const transListRef = ref(null);
|
| 446 |
+
// 自动滚动到底部的函数
|
| 447 |
+
const scrollToBottom = () => {
|
| 448 |
+
nextTick(() => {
|
| 449 |
+
if (transListRef.value) {
|
| 450 |
+
// @ts-ignore
|
| 451 |
+
transListRef.value.scrollTop = transListRef.value.scrollHeight + 144;
|
| 452 |
+
}
|
| 453 |
+
});
|
| 454 |
+
};
|
| 455 |
+
|
| 456 |
+
watch(() => [...completedNodesForDisplay], () => {
|
| 457 |
+
scrollToBottom();
|
| 458 |
+
}, { deep: true });
|
| 459 |
+
|
| 460 |
+
watch(() => current_node_trans_text.value, () => {
|
| 461 |
+
scrollToBottom();
|
| 462 |
+
}), { deep: true };
|
| 463 |
+
|
| 464 |
+
// config
|
| 465 |
+
const configVisible = ref(false);
|
| 466 |
+
const showConfig = () => {
|
| 467 |
+
configVisible.value = true;
|
| 468 |
+
}
|
| 469 |
+
const hideConfig = () => {
|
| 470 |
+
configVisible.value = false;
|
| 471 |
+
if (vadValueRef.value != settingsStore.$state.vad) {
|
| 472 |
+
settingsStore.$state.vad = vadValueRef.value
|
| 473 |
+
|
| 474 |
+
if (recorderRef.value) {
|
| 475 |
+
stopRecording();
|
| 476 |
+
sessionStore.endSession();
|
| 477 |
+
}
|
| 478 |
+
}
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
const vadValueRef = ref(0.3);
|
| 482 |
+
const maxWidthRef = ref(false);
|
| 483 |
+
const fontSizeRef = ref('trans-font-size-18')
|
| 484 |
+
const showRealTimeBufferRef = ref(true)
|
| 485 |
+
const showSourceLanguageOnlyRef = ref(false);
|
| 486 |
+
|
| 487 |
+
const onFontSizeChange = (e: any) => {
|
| 488 |
+
console.log('onFontSizeChange', e.target.value)
|
| 489 |
+
fontSizeRef.value = e.target.value
|
| 490 |
+
settingsStore.$state.fs = e.target.value
|
| 491 |
+
}
|
| 492 |
+
|
| 493 |
+
const sessionsModalVisible = ref(false);
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
// Modal 中下载按钮的处理函数
|
| 497 |
+
const handleDownloadSession = (summary: SessionSummary) => {
|
| 498 |
+
console.log(`请求下载会话: ${summary.startTime}`);
|
| 499 |
+
const nodes = sessionStore.loadSessionContent(summary.startTime);
|
| 500 |
+
if (nodes) {
|
| 501 |
+
// 弹出格式选择或直接下载 JSON
|
| 502 |
+
// const format = prompt("选择下载格式: 'json' 或 'txt'", "json");
|
| 503 |
+
// if (format === 'json' || format === 'txt') {
|
| 504 |
+
// downloadSessionData(summary.startTime, nodes, format);
|
| 505 |
+
// }
|
| 506 |
+
// downloadSessionData(summary.startTime, nodes, 'json'); // 默认下载 JSON
|
| 507 |
+
downloadText(summary.startTime, nodes);
|
| 508 |
+
|
| 509 |
+
} else {
|
| 510 |
+
alert(`无法加载会话 ${summary.startTime} 的内容进行下载。`);
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
// Modal 中删除按钮的处理函数
|
| 515 |
+
const handleDeleteSession = (summary: SessionSummary) => {
|
| 516 |
+
if (confirm(`确定要删除开始于 ${new Date(summary.startTime).toLocaleString()} 的会话吗?\n标题: ${summary.title}`)) {
|
| 517 |
+
sessionStore.deleteSession(summary.startTime);
|
| 518 |
+
console.log(`已删除会话: ${summary.startTime}`);
|
| 519 |
+
}
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
// 计算属性,用于模板中方便访问排序后的摘要
|
| 523 |
+
const sortedSummaries = computed(() => sessionStore.sortedSessionSummaries);
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
</script>
|
| 527 |
+
|
| 528 |
+
<template>
|
| 529 |
+
<div class="view-wrapper">
|
| 530 |
+
<div :class="['content-wrapper', maxWidthRef ? 'wrapper-width-auto': 'wrapper-width-fixed']">
|
| 531 |
+
<div style="margin-top: 0; padding: 0px">
|
| 532 |
+
<a-card :bordered="false" style="width: 100%;min-width: 100%;">
|
| 533 |
+
<div v-show="!(completedNodesForDisplay.length || current_node_text)" class="chat-box-placeholder">
|
| 534 |
+
{{
|
| 535 |
+
placeholder_en }}</div>
|
| 536 |
+
<div v-show="(completedNodesForDisplay.length || current_node_text)" class="trans-list"
|
| 537 |
+
ref="transListRef">
|
| 538 |
+
<div v-for="node in completedNodesForDisplay" :key="node.id" :class="['node']"
|
| 539 |
+
:data-seg-id="node.id">
|
| 540 |
+
<div
|
| 541 |
+
:class="[showSourceLanguageOnlyRef ? 'trans-dst-lang' : 'trans-src-lang', fontSizeRef]">
|
| 542 |
+
{{ node.text }}</div>
|
| 543 |
+
<div v-show="!showSourceLanguageOnlyRef" :class="['trans-dst-lang', fontSizeRef]">{{
|
| 544 |
+
node.translatedText }}</div>
|
| 545 |
+
</div>
|
| 546 |
+
|
| 547 |
+
<div v-show="showRealTimeBufferRef" class="node current_node"
|
| 548 |
+
:key="current_node_seg_id">
|
| 549 |
+
<div
|
| 550 |
+
:class="[showSourceLanguageOnlyRef ? 'trans-dst-lang' : 'trans-src-lang', fontSizeRef]">
|
| 551 |
+
{{ current_node_text }}</div>
|
| 552 |
+
<div v-show="!showSourceLanguageOnlyRef" :class="['trans-dst-lang', fontSizeRef]">{{
|
| 553 |
+
current_node_trans_text }}</div>
|
| 554 |
+
</div>
|
| 555 |
+
|
| 556 |
+
|
| 557 |
+
</div>
|
| 558 |
+
<template #actions>
|
| 559 |
+
<div class="actions-box">
|
| 560 |
+
<div class="left-actions">
|
| 561 |
+
<a-popover v-model:open="configVisible" placement="topLeft" trigger="click">
|
| 562 |
+
<template #content>
|
| 563 |
+
<div class="config-content">
|
| 564 |
+
<div v-if="false" class="config-block">
|
| 565 |
+
<h4 style="font-weight: 500;">Speaking Speed:</h4>
|
| 566 |
+
<a-radio-group v-model:value="vadValueRef">
|
| 567 |
+
<a-radio :value="0.1">fastest</a-radio>
|
| 568 |
+
<a-radio :value="0.3">fast</a-radio>
|
| 569 |
+
<a-radio :value="0.5">normal</a-radio>
|
| 570 |
+
<a-radio :value="0.75">slow</a-radio>
|
| 571 |
+
<a-radio :value="1">slowest</a-radio>
|
| 572 |
+
</a-radio-group>
|
| 573 |
+
</div>
|
| 574 |
+
<div v-if="false" class="config-block">
|
| 575 |
+
<h4 style="font-weight: 500;">Page Max Width:</h4>
|
| 576 |
+
<a-switch v-model:checked="maxWidthRef" />
|
| 577 |
+
</div>
|
| 578 |
+
<div class="config-block">
|
| 579 |
+
<h4 style="font-weight: 500;">Show Realtime Buffer:</h4>
|
| 580 |
+
<a-switch v-model:checked="showRealTimeBufferRef" />
|
| 581 |
+
</div>
|
| 582 |
+
<div class="config-block">
|
| 583 |
+
<h4 style="font-weight: 500;">Show Source Language Only:</h4>
|
| 584 |
+
<a-switch v-model:checked="showSourceLanguageOnlyRef" />
|
| 585 |
+
</div>
|
| 586 |
+
<div class="config-block">
|
| 587 |
+
<h4 style="font-weight: 500;">Text Font Size:</h4>
|
| 588 |
+
<a-radio-group v-model:value="fontSizeRef" @change="onFontSizeChange">
|
| 589 |
+
<a-radio :value="'trans-font-size-16'">Small</a-radio>
|
| 590 |
+
<a-radio :value="'trans-font-size-18'">Default</a-radio>
|
| 591 |
+
<a-radio :value="'trans-font-size-20'">Normal</a-radio>
|
| 592 |
+
<a-radio :value="'trans-font-size-24'">Medium</a-radio>
|
| 593 |
+
<a-radio :value="'trans-font-size-32'">Large</a-radio>
|
| 594 |
+
</a-radio-group>
|
| 595 |
+
</div>
|
| 596 |
+
</div>
|
| 597 |
+
<div style="display: flex; justify-content: end;">
|
| 598 |
+
<a-button type="primary" @click="hideConfig">Done</a-button>
|
| 599 |
+
</div>
|
| 600 |
+
</template>
|
| 601 |
+
<a-button type="dashed" shape="circle" size="middle" @click="showConfig">
|
| 602 |
+
<template #icon>
|
| 603 |
+
<SettingOutlined />
|
| 604 |
+
</template>
|
| 605 |
+
</a-button>
|
| 606 |
+
</a-popover>
|
| 607 |
+
|
| 608 |
+
<a-select v-model:value="transLanguageValue" style="width: 240px;"
|
| 609 |
+
placeholder="Select Language" :options="options"
|
| 610 |
+
@change="handleLanguageChange"></a-select>
|
| 611 |
+
<a-button type="dashed" shape="circle" size="middle"
|
| 612 |
+
@click="sessionsModalVisible = true">
|
| 613 |
+
<template #icon>
|
| 614 |
+
<FileTextOutlined />
|
| 615 |
+
</template>
|
| 616 |
+
</a-button>
|
| 617 |
+
<a-modal v-model:open="sessionsModalVisible" width="85vh" title="Session History"
|
| 618 |
+
centered :closable="true" ok-text="OK" @ok="sessionsModalVisible = false"
|
| 619 |
+
:footer="null">
|
| 620 |
+
<div class="sessions">
|
| 621 |
+
<div v-if="sortedSummaries.length > 0">
|
| 622 |
+
<div v-for="summary in sortedSummaries" :key="summary.startTime"
|
| 623 |
+
class="session-node">
|
| 624 |
+
<div class="content">
|
| 625 |
+
<!-- <div class="content-title">{{ summary.title }}</div> -->
|
| 626 |
+
<div class="content-text">
|
| 627 |
+
Start at: {{ new Date(summary.startTime).toLocaleString() }} ({{
|
| 628 |
+
summary.nodeCount }} items)
|
| 629 |
+
</div>
|
| 630 |
+
<div class="content-outline">
|
| 631 |
+
<div v-if="summary.outline.length > 0">
|
| 632 |
+
<div class="outline-line"
|
| 633 |
+
v-for="(line, index) in summary.outline" :key="index">{{
|
| 634 |
+
line
|
| 635 |
+
}}</div>
|
| 636 |
+
</div>
|
| 637 |
+
<i v-else>(No outline available)</i>
|
| 638 |
+
</div>
|
| 639 |
+
</div>
|
| 640 |
+
<div class="session-action">
|
| 641 |
+
<!-- <a-popconfirm title="Are you sure you want to delete this session?"
|
| 642 |
+
ok-text="Yes" cancel-text="No"
|
| 643 |
+
@confirm="handleDeleteSession(summary)">
|
| 644 |
+
<a-button type="danger" shape="circle" size="middle"
|
| 645 |
+
style="margin-left: 8px;">
|
| 646 |
+
<template #icon>
|
| 647 |
+
<DeleteOutlined />
|
| 648 |
+
</template>
|
| 649 |
+
</a-button>
|
| 650 |
+
</a-popconfirm> -->
|
| 651 |
+
<a-button danger type="dashed" shape="circle" size="middle"
|
| 652 |
+
@click="handleDeleteSession(summary)" style="margin-left: 8px;">
|
| 653 |
+
<template #icon>
|
| 654 |
+
<DeleteOutlined />
|
| 655 |
+
</template>
|
| 656 |
+
</a-button>
|
| 657 |
+
<a-button type="dashed" shape="circle" size="middle"
|
| 658 |
+
@click="handleDownloadSession(summary)">
|
| 659 |
+
<template #icon>
|
| 660 |
+
<DownloadOutlined />
|
| 661 |
+
</template>
|
| 662 |
+
</a-button>
|
| 663 |
+
</div>
|
| 664 |
+
</div>
|
| 665 |
+
</div>
|
| 666 |
+
</div>
|
| 667 |
+
</a-modal>
|
| 668 |
+
<!-- <a-popconfirm title="download session text content?" @confirm="downloadClick"
|
| 669 |
+
ok-text="Yes" cancel-text="No">
|
| 670 |
+
<template #icon>
|
| 671 |
+
<FileMarkdownOutlined style="color: red" />
|
| 672 |
+
</template>
|
| 673 |
+
<a-button type="dashed" shape="circle" size="middle">
|
| 674 |
+
<template #icon>
|
| 675 |
+
<DownloadOutlined />
|
| 676 |
+
</template>
|
| 677 |
+
</a-button>
|
| 678 |
+
</a-popconfirm> -->
|
| 679 |
+
</div>
|
| 680 |
+
|
| 681 |
+
<a-switch key="switcher" size="large" type="danger" checked-children="ON"
|
| 682 |
+
un-checked-children="OFF" v-model:checked="isRecording" @change="handleRecordingSwitch">
|
| 683 |
+
</a-switch>
|
| 684 |
+
<!-- <div class="right-actions">
|
| 685 |
+
<a-button type="dashed" shape="circle" size="middle" @click="downloadClick">
|
| 686 |
+
<template #icon>
|
| 687 |
+
<DownloadOutlined />
|
| 688 |
+
</template>
|
| 689 |
+
</a-button>
|
| 690 |
+
<a-switch key="switcher" size="large" type="danger" checked-children="ON"
|
| 691 |
+
un-checked-children="OFF" v-model:checked="isRecording"
|
| 692 |
+
@change="handleRecordingSwitch">
|
| 693 |
+
</a-switch>
|
| 694 |
+
</div> -->
|
| 695 |
+
|
| 696 |
+
</div>
|
| 697 |
+
</template>
|
| 698 |
+
</a-card>
|
| 699 |
+
</div>
|
| 700 |
+
</div>
|
| 701 |
+
</div>
|
| 702 |
+
</template>
|
| 703 |
+
|
| 704 |
+
<style lang="scss" scoped>
|
| 705 |
+
.config-content {
|
| 706 |
+
width: 320px;
|
| 707 |
+
margin:12px;
|
| 708 |
+
|
| 709 |
+
.config-block {
|
| 710 |
+
margin: 12px;
|
| 711 |
+
padding-bottom: 12px;
|
| 712 |
+
}
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.sessions {
|
| 716 |
+
width: 100%;
|
| 717 |
+
height: 100%;
|
| 718 |
+
min-height: 50vh;
|
| 719 |
+
max-height: 85vh;
|
| 720 |
+
overflow-y: scroll;
|
| 721 |
+
margin-top:24px;
|
| 722 |
+
display: flex;
|
| 723 |
+
flex-direction: column;
|
| 724 |
+
justify-content: flex-start;
|
| 725 |
+
|
| 726 |
+
.session-node {
|
| 727 |
+
width: 100%;
|
| 728 |
+
height: 100%;
|
| 729 |
+
display: flex;
|
| 730 |
+
justify-content: space-between;
|
| 731 |
+
align-items: center;
|
| 732 |
+
padding: 12px;
|
| 733 |
+
margin-bottom: 12px;
|
| 734 |
+
background-color: rgba(240, 241, 247, 1);
|
| 735 |
+
border-radius: 4px;
|
| 736 |
+
|
| 737 |
+
.content {
|
| 738 |
+
display: flex;
|
| 739 |
+
flex-direction: column;
|
| 740 |
+
justify-content: center;
|
| 741 |
+
align-items: self-start;
|
| 742 |
+
|
| 743 |
+
.content-title {
|
| 744 |
+
font-size: 18px;
|
| 745 |
+
font-weight: bold;
|
| 746 |
+
color: #2e2f33;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
.content-text {
|
| 750 |
+
font-size: 18px;
|
| 751 |
+
font-weight: 500;
|
| 752 |
+
color: #2e2f33;
|
| 753 |
+
}
|
| 754 |
+
.content-outline {
|
| 755 |
+
width: 100%;
|
| 756 |
+
|
| 757 |
+
.outline-line {
|
| 758 |
+
font-size: 16px;
|
| 759 |
+
font-weight: 500;
|
| 760 |
+
color: #909299;
|
| 761 |
+
margin: 8px 0 4px 0;
|
| 762 |
+
}
|
| 763 |
+
}
|
| 764 |
+
}
|
| 765 |
+
|
| 766 |
+
.session-action {
|
| 767 |
+
width: 96px;
|
| 768 |
+
display: flex;
|
| 769 |
+
justify-content: space-around;
|
| 770 |
+
align-items: center;
|
| 771 |
+
|
| 772 |
+
.ant-btn-primary {
|
| 773 |
+
background-color: #1890ff !important;
|
| 774 |
+
border-color: #1890ff !important;
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
}
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
.view-wrapper {
|
| 781 |
+
width: 100%;
|
| 782 |
+
height: 100%;
|
| 783 |
+
background-color: #fff;
|
| 784 |
+
|
| 785 |
+
.wrapper-width-fixed {
|
| 786 |
+
width: 100%;
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
.wrapper-width-auto {
|
| 790 |
+
width: 100vw;
|
| 791 |
+
}
|
| 792 |
+
|
| 793 |
+
.content-wrapper {
|
| 794 |
+
text-align: left;
|
| 795 |
+
max-width: 100vw;
|
| 796 |
+
min-width: 320px;
|
| 797 |
+
margin-bottom: 0;
|
| 798 |
+
height: 100%;
|
| 799 |
+
min-height: auto;
|
| 800 |
+
background-color: rgba(232, 232, 248, 0.8) !important;
|
| 801 |
+
|
| 802 |
+
.chat-box {
|
| 803 |
+
width: 100%;
|
| 804 |
+
height: calc(100vh - 112px);
|
| 805 |
+
|
| 806 |
+
// border: solid 1px lightgray;
|
| 807 |
+
border-radius: 4px;
|
| 808 |
+
padding: 12px;
|
| 809 |
+
color: #2e2f33;
|
| 810 |
+
font-size: 18px;
|
| 811 |
+
}
|
| 812 |
+
.chat-box-placeholder {
|
| 813 |
+
width: 100%;
|
| 814 |
+
height: calc(100vh - 112px);
|
| 815 |
+
border-radius: 4px;
|
| 816 |
+
padding: 12px;
|
| 817 |
+
font-size: 18px;
|
| 818 |
+
color: #a4a6ac;
|
| 819 |
+
}
|
| 820 |
+
|
| 821 |
+
.actions-box {
|
| 822 |
+
display: flex;
|
| 823 |
+
align-items: center;
|
| 824 |
+
justify-content: space-between;
|
| 825 |
+
margin: 0 36px;
|
| 826 |
+
height: 48px;
|
| 827 |
+
|
| 828 |
+
.left-actions {
|
| 829 |
+
display: flex;
|
| 830 |
+
align-items: center;
|
| 831 |
+
justify-content: space-between;
|
| 832 |
+
// width: 288px;
|
| 833 |
+
width: 332px;
|
| 834 |
+
}
|
| 835 |
+
|
| 836 |
+
.right-actions {
|
| 837 |
+
display: flex;
|
| 838 |
+
align-items: center;
|
| 839 |
+
justify-content: space-between;
|
| 840 |
+
width: 108px;
|
| 841 |
+
}
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
.trans-list {
|
| 845 |
+
overflow-y: auto;
|
| 846 |
+
width: 100%;
|
| 847 |
+
height: calc(100vh - 112px);
|
| 848 |
+
|
| 849 |
+
scrollbar-width: none;
|
| 850 |
+
-ms-overflow-style: none;
|
| 851 |
+
&::-webkit-scrollbar {
|
| 852 |
+
display: none;
|
| 853 |
+
}
|
| 854 |
+
|
| 855 |
+
.node {
|
| 856 |
+
margin-bottom: 36px;
|
| 857 |
+
width: 100% !important;
|
| 858 |
+
transition: all 0.3s ease;
|
| 859 |
+
|
| 860 |
+
.trans-time {
|
| 861 |
+
font-size: 14px;
|
| 862 |
+
color: #c4c6cc;
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
.trans-font-size-16 {
|
| 866 |
+
font-size: 16px;
|
| 867 |
+
}
|
| 868 |
+
.trans-font-size-18 {
|
| 869 |
+
font-size: 18px;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.trans-font-size-20 {
|
| 873 |
+
font-size: 20px;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.trans-font-size-32 {
|
| 877 |
+
font-size: 32px;
|
| 878 |
+
}
|
| 879 |
+
|
| 880 |
+
.trans-font-size-24 {
|
| 881 |
+
font-size: 24px;
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
|
| 885 |
+
.trans-src-lang {
|
| 886 |
+
// font-size: 18px;
|
| 887 |
+
color: #909299;
|
| 888 |
+
font-weight: 500;
|
| 889 |
+
}
|
| 890 |
+
|
| 891 |
+
.trans-dst-lang {
|
| 892 |
+
// font-size: 18px;
|
| 893 |
+
color: #2e2f33;
|
| 894 |
+
font-weight: 600;
|
| 895 |
+
}
|
| 896 |
+
}
|
| 897 |
+
|
| 898 |
+
.current_node {
|
| 899 |
+
background-color: rgba(240, 241, 247, 1);
|
| 900 |
+
padding: 8px 4px;
|
| 901 |
+
min-height: 68px;
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
}
|
| 905 |
+
}
|
| 906 |
+
}
|
| 907 |
+
|
| 908 |
+
// 动画关键帧定义 - 添加这部分
|
| 909 |
+
@keyframes highlight {
|
| 910 |
+
0% {
|
| 911 |
+
background-color: transparent;
|
| 912 |
+
}
|
| 913 |
+
|
| 914 |
+
50% {
|
| 915 |
+
background-color: rgba(255, 241, 206, 0.5);
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
100% {
|
| 919 |
+
background-color: transparent;
|
| 920 |
+
}
|
| 921 |
+
}
|
| 922 |
+
|
| 923 |
+
@keyframes slideIn {
|
| 924 |
+
from {
|
| 925 |
+
opacity: 0;
|
| 926 |
+
transform: translateY(10px);
|
| 927 |
+
}
|
| 928 |
+
|
| 929 |
+
to {
|
| 930 |
+
opacity: 1;
|
| 931 |
+
transform: translateY(0);
|
| 932 |
+
}
|
| 933 |
+
}
|
| 934 |
+
</style>
|
frontend/src/views/Settings/index.vue
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
|
| 3 |
+
import router from "@/router.ts";
|
| 4 |
+
import { useSettingsStore } from "@/stores/config.ts";
|
| 5 |
+
import {onMounted, ref} from "vue";
|
| 6 |
+
import {SettingTwoTone} from "@ant-design/icons-vue";
|
| 7 |
+
|
| 8 |
+
const settingsStore = useSettingsStore()
|
| 9 |
+
|
| 10 |
+
onMounted(() => {
|
| 11 |
+
console.log('config', settingsStore.$state)
|
| 12 |
+
})
|
| 13 |
+
const backAction = () => {
|
| 14 |
+
router.replace('/')
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// const inputType = ref<string>(settingsStore.$state.file_type);
|
| 18 |
+
// const role = ref<string>(settingsStore.$state.role_name);
|
| 19 |
+
// const onTypeChange = (e: any) => {
|
| 20 |
+
// console.log('onTypeChange', e.target.value)
|
| 21 |
+
// settingsStore.$state.file_type = e.target.value
|
| 22 |
+
// }
|
| 23 |
+
|
| 24 |
+
// const onRoleChange = (e: any) => {
|
| 25 |
+
// console.log('onRoleChange', e.target.value)
|
| 26 |
+
// settingsStore.$state.role_name = e.target.value
|
| 27 |
+
// stateStore.changeRole(e.target.value)
|
| 28 |
+
// console.log('role_name', settingsStore.$state.role_name)
|
| 29 |
+
// }
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
</script>
|
| 33 |
+
|
| 34 |
+
<template>
|
| 35 |
+
<div class="content-wrapper">
|
| 36 |
+
<a-result style="width: 100%;" title="Settings">
|
| 37 |
+
<!-- <template #icon>
|
| 38 |
+
<img
|
| 39 |
+
alt="logo"
|
| 40 |
+
src="/logo.webp"
|
| 41 |
+
style="width: 128px; border-radius: 24px;"
|
| 42 |
+
/>
|
| 43 |
+
</template>
|
| 44 |
+
<template #extra>
|
| 45 |
+
<div class="content-box">
|
| 46 |
+
<a-form layout="vertical">
|
| 47 |
+
<a-form-item
|
| 48 |
+
label="Choose your input type:"
|
| 49 |
+
name="inputType"
|
| 50 |
+
>
|
| 51 |
+
<a-radio-group v-model:value="inputType" @change="onTypeChange">
|
| 52 |
+
<a-radio :value="'file'">Audio File</a-radio>
|
| 53 |
+
<a-radio :value="'audio'">Speak</a-radio>
|
| 54 |
+
</a-radio-group>
|
| 55 |
+
</a-form-item>
|
| 56 |
+
|
| 57 |
+
<a-form-item
|
| 58 |
+
label="Choose your desire role:"
|
| 59 |
+
name="role"
|
| 60 |
+
>
|
| 61 |
+
<a-radio-group v-model:value="role" @change="onRoleChange">
|
| 62 |
+
<a-radio :value="'trump'">Trump</a-radio>
|
| 63 |
+
<a-radio :value="'ellen'">Ellen</a-radio>
|
| 64 |
+
</a-radio-group>
|
| 65 |
+
</a-form-item>
|
| 66 |
+
</a-form>
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
<a-button @click="backAction" type="primary">Back</a-button>
|
| 70 |
+
</template> -->
|
| 71 |
+
|
| 72 |
+
</a-result>
|
| 73 |
+
</div>
|
| 74 |
+
</template>
|
| 75 |
+
|
| 76 |
+
<style lang="scss" scoped>
|
| 77 |
+
|
| 78 |
+
.content-wrapper {
|
| 79 |
+
text-align: left;
|
| 80 |
+
max-width: 800px;
|
| 81 |
+
min-width: 320px;
|
| 82 |
+
margin-bottom: 64px;
|
| 83 |
+
min-height: calc(100vh - 438px);
|
| 84 |
+
|
| 85 |
+
.content-box {
|
| 86 |
+
padding: 24px;
|
| 87 |
+
height: 240px;
|
| 88 |
+
background-color: #e8e8e8;
|
| 89 |
+
border-radius: 16px;
|
| 90 |
+
width: 50%;
|
| 91 |
+
margin: 48px auto;
|
| 92 |
+
min-width: 300px;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.video-box {
|
| 96 |
+
max-width: 800px;
|
| 97 |
+
min-width: 320px;
|
| 98 |
+
width: 90vw;
|
| 99 |
+
height: auto;
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
</style>
|
frontend/src/views/Welcome/index.vue
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<script setup lang="ts">
|
| 2 |
+
|
| 3 |
+
import router from "@/router.ts";
|
| 4 |
+
import { useSettingsStore } from "@/stores/config.ts";
|
| 5 |
+
import { onMounted, ref, reactive, computed, h } from "vue";
|
| 6 |
+
import { Modal } from 'ant-design-vue';
|
| 7 |
+
import { SettingTwoTone } from "@ant-design/icons-vue";
|
| 8 |
+
import axios from "axios";
|
| 9 |
+
|
| 10 |
+
const base_url = axios.defaults.baseURL
|
| 11 |
+
|
| 12 |
+
const settingsStore = useSettingsStore()
|
| 13 |
+
|
| 14 |
+
import setting from "@/assets/setting.png"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
onMounted(async () => {
|
| 18 |
+
await fetchASRLanguages();
|
| 19 |
+
await fetchTTSRoles();
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
const chatAction = async () => {
|
| 23 |
+
const state = await startAudioChat();
|
| 24 |
+
if (!state) {
|
| 25 |
+
console.error('Failed to start audio chat system service');
|
| 26 |
+
|
| 27 |
+
Modal.error({
|
| 28 |
+
title: 'Error',
|
| 29 |
+
content: 'Failed to start audio chat system service',
|
| 30 |
+
});
|
| 31 |
+
return;
|
| 32 |
+
}
|
| 33 |
+
router.replace('/home')
|
| 34 |
+
}
|
| 35 |
+
const chatLoading = ref<boolean>(false);
|
| 36 |
+
|
| 37 |
+
const startAudioChat = async () => {
|
| 38 |
+
try {
|
| 39 |
+
chatLoading.value = true;
|
| 40 |
+
const response = await fetch(`${base_url}/system/start`, {
|
| 41 |
+
method: 'POST',
|
| 42 |
+
headers: {
|
| 43 |
+
'Content-Type': 'application/json',
|
| 44 |
+
},
|
| 45 |
+
body: null
|
| 46 |
+
});
|
| 47 |
+
if (!response.ok) {
|
| 48 |
+
|
| 49 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 50 |
+
}
|
| 51 |
+
const data = await response.json();
|
| 52 |
+
console.log('ASR Instance started successfully:', data);
|
| 53 |
+
return true;
|
| 54 |
+
} catch (error) {
|
| 55 |
+
console.error('Error starting ASR instance:', error);
|
| 56 |
+
return false;
|
| 57 |
+
} finally {
|
| 58 |
+
chatLoading.value = false;
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
const modelOpen = ref<boolean>(false);
|
| 64 |
+
const modalLoading = ref<boolean>(false);
|
| 65 |
+
|
| 66 |
+
const handleCancel = () => {
|
| 67 |
+
modelOpen.value = false;
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
const handleSubmit = async () => {
|
| 71 |
+
console.log('Selected Language:', language.value);
|
| 72 |
+
console.log('Selected Role:', role.value);
|
| 73 |
+
settingsStore.$state.language = language.value;
|
| 74 |
+
settingsStore.$state.role = role.value || '';
|
| 75 |
+
|
| 76 |
+
await pushConfig(settingsStore.$state.role);
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
const pushConfig = async (model_id: string) => {
|
| 80 |
+
try {
|
| 81 |
+
modalLoading.value = true;
|
| 82 |
+
const response = await fetch(`${base_url}/tts/models/load`, {
|
| 83 |
+
method: 'POST',
|
| 84 |
+
headers: {
|
| 85 |
+
'Content-Type': 'application/json',
|
| 86 |
+
},
|
| 87 |
+
body: JSON.stringify({
|
| 88 |
+
"model_id": model_id,
|
| 89 |
+
})
|
| 90 |
+
});
|
| 91 |
+
if (!response.ok) {
|
| 92 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 93 |
+
}
|
| 94 |
+
const data = await response.json();
|
| 95 |
+
console.log('Config pushed successfully:', data);
|
| 96 |
+
|
| 97 |
+
const response2 = await fetch(`${base_url}/asr/instance/create`, {
|
| 98 |
+
method: 'POST',
|
| 99 |
+
headers: {
|
| 100 |
+
'Content-Type': 'application/json',
|
| 101 |
+
},
|
| 102 |
+
body: JSON.stringify({
|
| 103 |
+
"language": language.value,
|
| 104 |
+
})
|
| 105 |
+
});
|
| 106 |
+
if (!response2.ok) {
|
| 107 |
+
throw new Error(`HTTP error! status: ${response2.status}`);
|
| 108 |
+
}
|
| 109 |
+
const data2 = await response2.json();
|
| 110 |
+
console.log('ASR Language set successfully:', data2);
|
| 111 |
+
|
| 112 |
+
} catch (err) {
|
| 113 |
+
console.error('Error pushing config:', err);
|
| 114 |
+
Modal.error({
|
| 115 |
+
title: 'Error',
|
| 116 |
+
content: "Error config: " + JSON.stringify(err),
|
| 117 |
+
});
|
| 118 |
+
} finally {
|
| 119 |
+
modalLoading.value = false;
|
| 120 |
+
modelOpen.value = false;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
console.log('Selected Language:', language.value);
|
| 124 |
+
console.log('Selected Role:', role.value);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
const language = ref<string>(settingsStore.$state.language || 'zh');
|
| 129 |
+
const languages = reactive([]);
|
| 130 |
+
const languageOptions = {
|
| 131 |
+
'zh': 'Chinese',
|
| 132 |
+
'en': 'English',
|
| 133 |
+
'auto': 'Auto',
|
| 134 |
+
};
|
| 135 |
+
const role = ref<string>(settingsStore.$state.role || '');
|
| 136 |
+
const roles = reactive([])
|
| 137 |
+
const radioStyle = reactive({
|
| 138 |
+
display: 'flex',
|
| 139 |
+
height: '48px',
|
| 140 |
+
lineHeight: '48px',
|
| 141 |
+
fontSize: '18px',
|
| 142 |
+
});
|
| 143 |
+
|
| 144 |
+
const filteredRoles = computed(() => {
|
| 145 |
+
const is_chinese = language.value == 'zh';
|
| 146 |
+
return roles.filter(ro => ro['is_chinese_voice'] == is_chinese);
|
| 147 |
+
});
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
const fetchTTSRoles = async () => {
|
| 151 |
+
try {
|
| 152 |
+
const response = await fetch(`${base_url}/tts/models`);
|
| 153 |
+
const data = await response.json()
|
| 154 |
+
if (data && data.models) {
|
| 155 |
+
// @ts-ignore
|
| 156 |
+
roles.splice(0, data.length, ...data.models)
|
| 157 |
+
console.log('Fetched TTS Roles:', roles);
|
| 158 |
+
}
|
| 159 |
+
} catch (error) {
|
| 160 |
+
console.error('Error fetching TTS roles:', error);
|
| 161 |
+
}
|
| 162 |
+
};
|
| 163 |
+
|
| 164 |
+
const fetchASRLanguages = async () => {
|
| 165 |
+
try {
|
| 166 |
+
const response = await fetch(`${base_url}/asr/languages`);
|
| 167 |
+
const data = await response.json();
|
| 168 |
+
if (data && data.languages) {
|
| 169 |
+
// @ts-ignore
|
| 170 |
+
languages.splice(0, languages.length, ...data.languages);
|
| 171 |
+
console.log('Fetched ASR Languages:', data.languages);
|
| 172 |
+
}
|
| 173 |
+
} catch (error) {
|
| 174 |
+
console.error('Error fetching ASR languages:', error);
|
| 175 |
+
}
|
| 176 |
+
};
|
| 177 |
+
|
| 178 |
+
const toggleSider = () => {
|
| 179 |
+
settingsStore.$patch({ sider_open: !settingsStore.$state.sider_open });
|
| 180 |
+
console.log('sider open: ', modelOpen.value);
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
</script>
|
| 184 |
+
|
| 185 |
+
<template>
|
| 186 |
+
<div class="welcome-wrapper">
|
| 187 |
+
<div class="content">
|
| 188 |
+
<div class="inner-content">
|
| 189 |
+
<div class="text-box">
|
| 190 |
+
<div class="title">
|
| 191 |
+
欢迎使用
|
| 192 |
+
</div>
|
| 193 |
+
<div class="sub-title">
|
| 194 |
+
点击下方按钮开始对话
|
| 195 |
+
</div>
|
| 196 |
+
</div>
|
| 197 |
+
<div class="btn-box">
|
| 198 |
+
<a-button @click="chatAction" block :loading="chatLoading" type="primary" size="large">
|
| 199 |
+
<span>开始对话</span>
|
| 200 |
+
</a-button>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<div class="actions">
|
| 206 |
+
<!-- <a-button type="text" @click="toggleSider">sider</a-button> -->
|
| 207 |
+
|
| 208 |
+
<a-button type="text" @click="modelOpen = true"
|
| 209 |
+
style="width:44px; height: 44px; margin-right:24px;margin-bottom: 24px;">
|
| 210 |
+
<template #icon>
|
| 211 |
+
<img :src="setting" width="28" height="28" alt="settings" />
|
| 212 |
+
</template>
|
| 213 |
+
</a-button>
|
| 214 |
+
</div>
|
| 215 |
+
|
| 216 |
+
<a-modal v-model:open="modelOpen" :title="null">
|
| 217 |
+
<template #footer>
|
| 218 |
+
<a-button key="back" @click="handleCancel">Cancel</a-button>
|
| 219 |
+
<a-button key="submit" type="primary" :loading="modalLoading" @click="handleSubmit">Submit</a-button>
|
| 220 |
+
</template>
|
| 221 |
+
<div class="languages">
|
| 222 |
+
<div class="language-item">
|
| 223 |
+
<p>Select Language:</p>
|
| 224 |
+
<a-select v-model:value="language" style="width: 100%;">
|
| 225 |
+
<a-select-option v-for="lan in languages" :value="lan" :key="lan">
|
| 226 |
+
{{ languageOptions[lan] }}
|
| 227 |
+
</a-select-option>
|
| 228 |
+
</a-select>
|
| 229 |
+
</div>
|
| 230 |
+
</div>
|
| 231 |
+
<div class="languages">
|
| 232 |
+
<div class="role-item">
|
| 233 |
+
<p>Select voice Role:</p>
|
| 234 |
+
<a-radio-group size="large" v-model:value="role">
|
| 235 |
+
<a-radio v-for="r in filteredRoles" :style="radioStyle" :value="r['id']" :key="r['id']">
|
| 236 |
+
{{ r['character_name'] }}
|
| 237 |
+
</a-radio>
|
| 238 |
+
</a-radio-group>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
</a-modal>
|
| 242 |
+
</div>
|
| 243 |
+
</template>
|
| 244 |
+
|
| 245 |
+
<style lang="scss" scoped>
|
| 246 |
+
|
| 247 |
+
.languages {
|
| 248 |
+
margin-top: 40px;
|
| 249 |
+
margin-bottom: 48px;
|
| 250 |
+
|
| 251 |
+
p {
|
| 252 |
+
font-size: 20px;
|
| 253 |
+
font-weight: 500;
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.welcome-wrapper {
|
| 258 |
+
width: 100%;
|
| 259 |
+
height: 100%;
|
| 260 |
+
background-image: url('@/assets/bg.png');
|
| 261 |
+
background-repeat: no-repeat;
|
| 262 |
+
background-attachment: fixed;
|
| 263 |
+
background-size: cover;
|
| 264 |
+
background-position: center;
|
| 265 |
+
display: flex;
|
| 266 |
+
flex-direction: column;
|
| 267 |
+
align-items: center;
|
| 268 |
+
justify-content: space-between;
|
| 269 |
+
color: #fff;
|
| 270 |
+
|
| 271 |
+
.content {
|
| 272 |
+
width: 100%;
|
| 273 |
+
height: 80vh;
|
| 274 |
+
display: flex;
|
| 275 |
+
flex-direction: column;
|
| 276 |
+
justify-content: space-around;
|
| 277 |
+
margin-top: 64px;
|
| 278 |
+
|
| 279 |
+
.inner-content {
|
| 280 |
+
display: flex;
|
| 281 |
+
flex-direction: column;
|
| 282 |
+
align-items: center;
|
| 283 |
+
justify-content: center;
|
| 284 |
+
text-align: center;
|
| 285 |
+
padding: 20px;
|
| 286 |
+
|
| 287 |
+
.text-box {
|
| 288 |
+
color: #000;
|
| 289 |
+
margin-bottom: 36px;
|
| 290 |
+
|
| 291 |
+
.title {
|
| 292 |
+
font-size: 24px;
|
| 293 |
+
font-weight: 600;
|
| 294 |
+
margin-bottom: 24px;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.sub-title {
|
| 298 |
+
font-size: 15px;
|
| 299 |
+
margin-top: 10px;
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
.btn-box {
|
| 303 |
+
width: 224px;
|
| 304 |
+
height: 80px;
|
| 305 |
+
}
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.actions {
|
| 310 |
+
width: 100%;;
|
| 311 |
+
height: 64px;
|
| 312 |
+
|
| 313 |
+
display: flex;
|
| 314 |
+
justify-content: flex-end;
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
</style>
|
frontend/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
frontend/tailwind.config.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d38aba25b9f08679e006dc4e47140de2b570399219a5c7c85ced3636b9d054c2
|
| 3 |
+
size 187
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3e53114fb8daa29e3d6b6d066a225489196535decbbe4e81ec7281986c6188da
|
| 3 |
+
size 770
|
frontend/tsconfig.node.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9e2abb169ea87b7190613a1d4da57ca608463a453bd4231fa3aeee5e308370dd
|
| 3 |
+
size 213
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import vue from '@vitejs/plugin-vue'
|
| 3 |
+
import { resolve } from 'path'
|
| 4 |
+
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
| 5 |
+
|
| 6 |
+
const absPath = (fp: string): string => {
|
| 7 |
+
return resolve(__dirname, fp)
|
| 8 |
+
}
|
| 9 |
+
// https://vitejs.dev/config/
|
| 10 |
+
export default defineConfig({
|
| 11 |
+
define: {
|
| 12 |
+
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'true'
|
| 13 |
+
},
|
| 14 |
+
base: "./",
|
| 15 |
+
build: {
|
| 16 |
+
outDir: 'www',
|
| 17 |
+
},
|
| 18 |
+
plugins: [vue({
|
| 19 |
+
script: {
|
| 20 |
+
defineModel: true
|
| 21 |
+
}
|
| 22 |
+
}),
|
| 23 |
+
// viteStaticCopy({
|
| 24 |
+
// targets: [
|
| 25 |
+
// {
|
| 26 |
+
// src: 'node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js',
|
| 27 |
+
// dest: './assets/'
|
| 28 |
+
// },
|
| 29 |
+
// {
|
| 30 |
+
// src: 'node_modules/@ricky0123/vad-web/dist/silero_vad.onnx',
|
| 31 |
+
// dest: './assets/'
|
| 32 |
+
// },
|
| 33 |
+
// {
|
| 34 |
+
// src: 'node_modules/onnxruntime-web/dist/*.wasm',
|
| 35 |
+
// dest: './assets/'
|
| 36 |
+
// },
|
| 37 |
+
// {
|
| 38 |
+
// src: 'node_modules/onnxruntime-web/dist/*.mjs',
|
| 39 |
+
// dest: './assets/'
|
| 40 |
+
// }
|
| 41 |
+
// ]
|
| 42 |
+
// })
|
| 43 |
+
],
|
| 44 |
+
assetsInclude: [
|
| 45 |
+
"**/*.txt",
|
| 46 |
+
],
|
| 47 |
+
resolve: {
|
| 48 |
+
alias: {
|
| 49 |
+
// @ is an alias to /src
|
| 50 |
+
'@': absPath('src'),
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
})
|