Commit d9ea3ea9 authored by ali's avatar ali

feat: 照片数字人展示对话内容

parent 8a378618
<script setup lang="ts">
import { ref } from 'vue';
interface Props {
visible?: boolean;
question?: string;
loading?: boolean;
answerData: {
answerType: string;
data: string[];
subType: 'text' | 'image' | 'video' | 'relatelist' | 'guslist' | 'waitImage';
value: string;
}[];
}
interface EmitType {
(e: 'inputQuestion', value: string): void;
}
const emits = defineEmits<EmitType>();
withDefaults(defineProps<Props>(), {
answerData() {
return [];
},
visible: true,
loading: false,
question: ''
});
const isPreview = ref(false);
const previewUrl = ref('');
function previewImg(url: string) {
previewUrl.value = url;
isPreview.value = true;
}
const handleUrl = (url: string) => {
if (url === '') return '';
return `https://bf-saas.emotibot.com${url}`;
};
function inputQuestion(t: string) {
emits('inputQuestion', t);
}
const isPlay = ref(false);
function playVideo() {
(document.querySelector('#video') as HTMLVideoElement).play();
isPlay.value = true;
}
const loadingImg = new URL('/images/Ellipsis.gif', import.meta.url).href;
</script>
<template>
<div
:style="{ width: '170px' }"
class="ans-layout"
>
<div
v-show="question && question !== ''"
class="question-box"
>
<div class="question-con">
<div class="scroll">{{ question }}</div>
</div>
</div>
<div
v-show="answerData.length > 0 && visible"
class="answer-box vertical"
>
<div
class="scroll"
:style="{ 'max-height': '249.6px' }"
>
<div
v-for="(item, index) of answerData"
:key="index"
class="ans-item"
>
<!-- 文字 -->
<div
v-if="item.subType === 'text'"
class="text-box"
v-html="item.value"
></div>
<!-- 图片 -->
<div
v-else-if="item.subType === 'image'"
class="img-box"
>
<img
:src="handleUrl(item.value)"
@click="previewImg(handleUrl(item.value))"
/>
</div>
<!-- 视频 -->
<div
v-else-if="item.subType === 'video'"
class="video-box"
>
<video
id="video"
controls
:src="handleUrl(item.value)"
></video>
<div
v-show="!isPlay"
class="play-mask"
>
<div
class="play"
@click="playVideo()"
>
<img src="@/assets/helper/play.svg" />
</div>
</div>
</div>
<!-- 相似问 -->
<div
v-else-if="item.subType === 'relatelist' || item.subType === 'guslist'"
class="recommend-box"
>
<div class="text">{{ item.value }}</div>
<ul class="glist">
<li
v-for="(gitem, gindex) of item.data"
:key="gindex"
@click="inputQuestion(gitem)"
>{{
`${gindex + 1}. ${gitem}`
}}</li>
</ul>
</div>
<!-- 等待gif -->
<div
v-if="loading"
class="text-waitImage"
>
<img :src="loadingImg" />
</div>
</div>
</div>
</div>
</div>
<teleport to="body">
<div
v-if="isPreview"
class="preview-box"
@click="isPreview = false"
>
<div
class="img"
:style="{ backgroundImage: `url(${previewUrl})` }"
></div>
</div>
</teleport>
</template>
<style scoped >
.ans-layout {
position: fixed;
top: 50%;
right: 30px;
transform: translateY(-130px);
}
.question-box {
width: 100%;
display: flex;
justify-content: right;
color: #FFFFFF;
margin-bottom: 10px;
}
.question-box .question-con {
max-width: 100%;
background: rgba(70, 121, 254, 0.7);
border-radius: 30px;
padding: 15px;
}
.question-box .question-con .scroll {
max-height: 57px;
}
.answer-box {
background: rgba(255, 255, 255, 0.7);
border-radius: 30px;
border: 1px solid #ffffff;
color: #333333;
font-weight: 400;
}
.answer-box.horizontal {
padding: 14px 13px;
}
.answer-box.vertical {
padding: 16px;
}
.text-box {
margin-bottom: 15px;
}
.img-box,
.video-box {
width: 100%;
border-radius: 4px 4px 4px 4px;
overflow: hidden;
margin-bottom: 15px;
position: relative;
}
.img-box img,
.video-box img,
.img-box video,
.video-box video {
width: 100%;
}
.play-mask {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
cursor: pointer;
}
.text-waitImage img {
width: 20%;
margin: 0 auto;
display: block;
}
.play {
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
opacity: 0.75;
}
.preview-box {
width: 100%;
height: 100%;
position: fixed;
background: rgba(0, 0, 0, 0.3);
top: 0;
left: 0;
z-index: 3;
display: flex;
align-items: center;
justify-content: center;
}
.preview-box .img {
width: 80%;
height: 80%;
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
}
.glist > li {
cursor: pointer;
text-decoration: underline;
font-weight: 400;
color: #6464f8;
line-height: 24px;
}
.glist > li:hover,
.glist > li:active {
color: #2eacff;
}
.scroll {
height: 100%;
overflow-y: auto;
}
.scroll::-webkit-scrollbar {
width: 6px;
height: 8px;
}
.scroll::-webkit-scrollbar-track {
background: transparent;
border-radius: 4px;
}
.scroll::-webkit-scrollbar-thumb {
background: #d8dee9;
border-radius: 4px;
}
.scroll::-webkit-scrollbar-thumb:hover {
background: #bdbdbe;
}
@media (width: 254px) {
.ans-layout {
min-width: 150px;
transform: translateY(-50%) scale(0.78) !important;
transform-origin: left center;
}
}
</style>
<!-- eslint-disable no-unused-vars --> <!-- eslint-disable no-unused-vars -->
<!-- eslint-disable camelcase --> <!-- eslint-disable camelcase -->
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, ref } from 'vue' import { onMounted, ref, type Ref } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import type { import type {
ServerMessagePartialResult, ServerMessagePartialResult,
...@@ -12,6 +12,7 @@ import { audioAiTTS, localTTS } from '../plugins/tts' ...@@ -12,6 +12,7 @@ import { audioAiTTS, localTTS } from '../plugins/tts'
import useStore from '@/renderer/store' import useStore from '@/renderer/store'
import flvjs from 'flv.js' import flvjs from 'flv.js'
import { PhotoAnswer, PhotoRole } from '@/renderer/plugins/live/PhotoRole' import { PhotoAnswer, PhotoRole } from '@/renderer/plugins/live/PhotoRole'
import AnswerBox from "@/renderer/components/common/AnswerBox.vue";
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
...@@ -37,10 +38,22 @@ const inputContext: { ...@@ -37,10 +38,22 @@ const inputContext: {
asrPartial: string asrPartial: string
answerArray: { text: string; isLast: boolean }[] answerArray: { text: string; isLast: boolean }[]
steps: Promise<string>[] steps: Promise<string>[]
answerProp: Ref<{
visible: boolean;
question: string;
loading: boolean
answerData: {
answerType: string;
data: string[];
subType: 'text' | 'image' | 'video' | 'relatelist' | 'guslist';
value: string;
}[]
}>
} = { } = {
asrPartial: '', asrPartial: '',
answerArray: [], answerArray: [],
steps: [] steps: [],
answerProp: ref({ question: '', loading: false, visible: false, answerData: [] })
} }
router.beforeEach((g) => { router.beforeEach((g) => {
...@@ -102,6 +115,10 @@ async function init() { ...@@ -102,6 +115,10 @@ async function init() {
} }
async function onAsyncAnswer(ans: PhotoAnswer) { async function onAsyncAnswer(ans: PhotoAnswer) {
if (inputContext.answerProp.value.answerData[0]) {
inputContext.answerProp.value.answerData[0].value = ans.asyncAnswer;
}
if (ans.playState === 'playing') { if (ans.playState === 'playing') {
microphoneState.value = 'reply' microphoneState.value = 'reply'
return return
...@@ -259,6 +276,10 @@ async function endAudioInput() { ...@@ -259,6 +276,10 @@ async function endAudioInput() {
inputContext.asrPartial = '' inputContext.asrPartial = ''
inputContext.answerArray.length = 0 inputContext.answerArray.length = 0
inputContext.steps.length = 0 inputContext.steps.length = 0
inputContext.answerProp.value.answerData.length = 0;
inputContext.answerProp.value.visible = false;
inputContext.answerProp.value.question = '';
inputContext.answerProp.value.loading = false;
// @ts-ignore // @ts-ignore
photoRole?.off('asyncAnswer', onAsyncAnswer) photoRole?.off('asyncAnswer', onAsyncAnswer)
await photoRole?.destroy() await photoRole?.destroy()
...@@ -299,6 +320,8 @@ async function onQ(question: string) { ...@@ -299,6 +320,8 @@ async function onQ(question: string) {
console.log('----------------> question: ', question) console.log('----------------> question: ', question)
microphoneState.value = 'loading' microphoneState.value = 'loading'
inputContext.answerProp.value.question = question;
inputContext.answerProp.value.loading = true;
const { pose, stepResolve, stepReject } = createStep() const { pose, stepResolve, stepReject } = createStep()
inputContext.steps.length = 0 inputContext.steps.length = 0
...@@ -334,6 +357,13 @@ async function llmLoop(question: string) { ...@@ -334,6 +357,13 @@ async function llmLoop(question: string) {
} }
photoRole!.answerArgs = answer photoRole!.answerArgs = answer
inputContext.answerProp.value.visible = true;
inputContext.answerProp.value.answerData.push({
subType: 'text',
value: '',
answerType: '',
data: []
})
// @ts-ignore // @ts-ignore
photoRole!.off('asyncAnswer', onAsyncAnswer) photoRole!.off('asyncAnswer', onAsyncAnswer)
photoRole!.on('asyncAnswer', onAsyncAnswer) photoRole!.on('asyncAnswer', onAsyncAnswer)
...@@ -363,6 +393,7 @@ async function llmLoop(question: string) { ...@@ -363,6 +393,7 @@ async function llmLoop(question: string) {
const audioList = results[0].audio_list as string[] const audioList = results[0].audio_list as string[]
if (audioList.length === 0) continue if (audioList.length === 0) continue
const isEnd = audioList.at(-1) === 'stream_end' const isEnd = audioList.at(-1) === 'stream_end'
inputContext.answerProp.value.loading = !isEnd
if (isEnd) audioList.pop() if (isEnd) audioList.pop()
...@@ -482,6 +513,8 @@ async function down() { ...@@ -482,6 +513,8 @@ async function down() {
<v-btn color="red" variant="text" @click="errorSnackbar = false"> Close </v-btn> <v-btn color="red" variant="text" @click="errorSnackbar = false"> Close </v-btn>
</template> </template>
</v-snackbar> </v-snackbar>
<answer-box :loading="inputContext.answerProp.value.loading" :visible="inputContext.answerProp.value.visible" :question="inputContext.answerProp.value.question" :answer-data="inputContext.answerProp.value.answerData"></answer-box>
</template> </template>
<style scoped> <style scoped>
.voice { .voice {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment