Merge branches 'main' and 'main' of ssh://18.167.251.121:10002/aidlab/lanecarford_front

This commit is contained in:
X1627315083
2025-11-18 11:37:24 +08:00
14 changed files with 429 additions and 53 deletions

View File

@@ -49,6 +49,25 @@ export function generateTryOnEffect(data: Object) {
data, data,
}) })
} }
/**
* 生成试穿效果-演示
* @param data 试穿效果数据
* @param data.customerId 顾客ID
* @param data.visitRecordId 进店记录id
* @param data.styleId 样式id
* @param data.modelPhotoId 模型照片id
* @param data.customerPhotoId 顾客照片id
* @param data.prompt 提示词
* @param data.originalTryOnId 原始试穿效果id
* @param data.isRegenerated 是否重新生成 0-否1-是
*/
export function generateTryOnEffectDemo(data: Object) {
return request({
url: '/api/try-on-effects/reFace/{customerPhotold}o',
method: 'post',
data,
})
}
/** 上传图片-AI换脸 /** 上传图片-AI换脸
* @param data 图片数据 * @param data 图片数据

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 KiB

View File

@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import { useGenerateStore } from '@/stores/modules/generate' import { useGenerateStore } from '@/stores/modules/generate'
const VerifyIDs = (num: number) => { const VerifyIDs = (num: number) => {
return true
const ids = [ const ids = [
!!useGenerateStore().customerId, !!useGenerateStore().customerId,
!!useGenerateStore().visitRecordId, !!useGenerateStore().visitRecordId,
@@ -130,6 +131,13 @@ const router = createRouter({
component: () => import('../views/Workshop/product.vue'), component: () => import('../views/Workshop/product.vue'),
meta: { verify: ()=> VerifyIDs(4) } meta: { verify: ()=> VerifyIDs(4) }
}, },
{
// 推荐try on
path: '/workshop/recommended',
name: 'recommended',
component: () => import('../views/Workshop/recommended.vue'),
meta: { verify: ()=> VerifyIDs(5) }
},
{ {
// 上传照片1 // 上传照片1
path: '/workshop/uploadFace', path: '/workshop/uploadFace',

View File

@@ -42,6 +42,19 @@ export const useGenerateStore = defineStore({
isRegenerated: '', isRegenerated: '',
isFavorite: false isFavorite: false
}, },
/** AI魔改信息-演示 */
customizeInfoDemo: {
inputText: '',
count: 0,
oldInputText: '',
oldTryOnId: '',
tryOnId: '',
tryOnUrl: '',
styleUrl: '',
isRegenerated: '',
isFavorite: false
},
customerInfo: { customerInfo: {
customerId: '', customerId: '',
visitRecordId: '' visitRecordId: ''
@@ -126,6 +139,18 @@ export const useGenerateStore = defineStore({
this.customizeInfo.styleUrl = '' this.customizeInfo.styleUrl = ''
this.customizeInfo.isRegenerated = '' this.customizeInfo.isRegenerated = ''
this.customizeInfo.isFavorite = false this.customizeInfo.isFavorite = false
},
/** 清空 AI魔改信息-演示 */
clearCustomizeInfoDemo() {
this.customizeInfoDemo.inputText = ''
this.customizeInfoDemo.count = 0
this.customizeInfoDemo.oldInputText = ''
this.customizeInfoDemo.oldTryOnId = ''
this.customizeInfoDemo.tryOnId = ''
this.customizeInfoDemo.tryOnUrl = ''
this.customizeInfoDemo.styleUrl = ''
this.customizeInfoDemo.isRegenerated = ''
this.customizeInfoDemo.isFavorite = false
}, },
uploadCustomizeInfo(data: object) { uploadCustomizeInfo(data: object) {
for (const key in data) { for (const key in data) {
@@ -143,6 +168,7 @@ export const useGenerateStore = defineStore({
this.clearProductData() this.clearProductData()
this.updatePhotoInfo({}) this.updatePhotoInfo({})
this.clearCustomizeInfo() this.clearCustomizeInfo()
this.clearCustomizeInfoDemo()
this.clearCustomerInfo() this.clearCustomerInfo()
this.setSessionId('') this.setSessionId('')
}, },

View File

@@ -2,19 +2,22 @@
import HeaderTitle from '@/components/HeaderTitle.vue' import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue' import FooterNavigation from '@/components/FooterNavigation.vue'
import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue' import GenerateLoading from '@/views/asistant/components/GenerateLoading.vue'
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { import {
generateTryOnEffect, generateTryOnEffect,
generateTryOnEffectDemo,
setTryOnEffectFavorite, setTryOnEffectFavorite,
cancelTryOnEffectFavorite cancelTryOnEffectFavorite
} from '@/api/workshop' } from '@/api/workshop'
const emit = defineEmits(['viewType']) const emit = defineEmits(['viewType'])
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useGenerateStore } from '@/stores' import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore() const generateStore = useGenerateStore()
const customizeInfo = generateStore.customizeInfo
const router = useRouter() const router = useRouter()
const route = useRoute()
const isDemo = computed(() => route.query.demo === '1')
const customizeInfo = isDemo.value ? generateStore.customizeInfoDemo : generateStore.customizeInfo
const loading = ref(false) const loading = ref(false)
const onSend = () => { const onSend = () => {
if (customizeInfo.inputText === '') return if (customizeInfo.inputText === '') return
@@ -45,8 +48,8 @@
} }
if (generateStore.customerPhotoId && customizeInfo.count === 0) if (generateStore.customerPhotoId && customizeInfo.count === 0)
data['customerPhotoId'] = generateStore.customerPhotoId data['customerPhotoId'] = generateStore.customerPhotoId
loading.value = true loading.value = true;
generateTryOnEffect(data) (isDemo.value ? generateTryOnEffectDemo : generateTryOnEffect)(data)
.then((res: any) => { .then((res: any) => {
customizeInfo.count++ customizeInfo.count++
customizeInfo.tryOnId = res.tryOnId customizeInfo.tryOnId = res.tryOnId
@@ -66,6 +69,7 @@
// 喜欢 // 喜欢
const isLoveLoading = ref(false) const isLoveLoading = ref(false)
const onLove = () => { const onLove = () => {
if (isDemo.value) return
if (isLoveLoading.value) return if (isLoveLoading.value) return
const http = customizeInfo.isFavorite ? cancelTryOnEffectFavorite : setTryOnEffectFavorite const http = customizeInfo.isFavorite ? cancelTryOnEffectFavorite : setTryOnEffectFavorite
customizeInfo.isFavorite = !customizeInfo.isFavorite customizeInfo.isFavorite = !customizeInfo.isFavorite
@@ -86,7 +90,11 @@
router.back() router.back()
} }
const onFinish = () => { const onFinish = () => {
router.push({ name: 'creation' }) if (isDemo.value) {
router.push({ name: 'end' })
} else {
router.push({ name: 'creation' })
}
} }
</script> </script>
@@ -114,7 +122,7 @@
<div class="icon"><SvgIcon name="xialajiantou" size="29" /></div> <div class="icon"><SvgIcon name="xialajiantou" size="29" /></div>
</div> </div>
<div class="icons"> <div class="icons">
<div @click="onLove"> <div @click="onLove" v-if="!isDemo">
<SvgIcon :name="`love_${customizeInfo.isFavorite ? 1 : 0}`" size="35" /> <SvgIcon :name="`love_${customizeInfo.isFavorite ? 1 : 0}`" size="35" />
</div> </div>
<div @click="onReload" v-show="customizeInfo.oldInputText"> <div @click="onReload" v-show="customizeInfo.oldInputText">

View File

@@ -4,6 +4,8 @@ import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue' import FooterNavigation from '@/components/FooterNavigation.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
import { showConfirmDialog } from 'vant'
import MyEvent from '@/utils/myEvent'
//const props = defineProps({ //const props = defineProps({
//}) //})
@@ -14,6 +16,21 @@ const emit = defineEmits([
// const data = reactive({ // const data = reactive({
// }) // })
const clickSwitchVIPID = ()=>{
showConfirmDialog({
title: 'Switch VIP ID?',
message: 'You have unsaved changes. Your progress will be lost.',
confirmButtonText: 'Yes',
cancelButtonText: 'Cancel',
})
.then(() => {
MyEvent.emit('clear-generate-state')
MyEvent.emit('clearAllCache')
router.push('/stylist/customer')
})
.catch(() => {})
}
onMounted(()=>{ onMounted(()=>{
emit('view-type', 1) emit('view-type', 1)
}) })
@@ -37,13 +54,13 @@ defineExpose({})
<div class="item" @click="()=>router.push('/stylist/index')"> <div class="item" @click="()=>router.push('/stylist/index')">
<img src="@/assets/images/nav1.png" alt=""> <img src="@/assets/images/nav1.png" alt="">
</div> </div>
<div class="item" @click="()=>router.push('/workshop/uploadFace')"> <div class="item" @click="()=>router.push('/workshop/recommended')">
<img src="@/assets/images/nav2.png" alt=""> <img src="@/assets/images/nav2.png" alt="">
</div> </div>
<div class="item" @click="()=>router.push('/stylist/index')"> <div class="item" @click="()=>router.push('/stylist/index')">
<img src="@/assets/images/nav3.png" alt=""> <img src="@/assets/images/nav3.png" alt="">
</div> </div>
<div class="item" @click="()=>router.push('/stylist/customer')"> <div class="item" @click="clickSwitchVIPID">
<img src="@/assets/images/nav4.png" alt=""> <img src="@/assets/images/nav4.png" alt="">
</div> </div>
</div> </div>

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore()
const emit = defineEmits(['view-type'])
onMounted(() => {
emit('view-type', 1)
})
const router = useRouter()
const clickNext = () => {
generateStore.updatePhotoInfo({})
router.push({ name: 'uploadFace', query: { demo: 1 } })
}
</script>
<template>
<header-title />
<!-- 上传照片 -->
<div class="upload-face-1">
<img src="@/assets/images/workshop/bg/upload_bg.png" class="bg" />
<div class="content">
<div class="texts">
<p class="title">Recommended<br />Try-on</p>
<p class="desc">Come and try face swapping!</p>
</div>
<div class="image">
<img src="../../assets/images/workshop/template.png" />
</div>
<button class="sandblasted-blurred" @click="clickNext"><span>Next</span></button>
</div>
</div>
<footer-navigation is-placeholder />
</template>
<style scoped lang="less">
.upload-face-1 {
width: 100%;
flex: 1;
overflow: hidden;
position: relative;
color: #fff;
> .bg {
position: absolute;
width: 100%;
height: auto;
min-height: 100%;
object-fit: cover;
}
> .content {
margin-top: 12rem;
text-align: center;
> .texts {
// left: 0;
width: 100%;
// padding: 9.9rem 0 0 7.2rem;
> .title {
font-family: 'satoshiBold';
font-size: 11rem;
line-height: 124%;
}
> .desc {
font-family: 'satoshiMedium';
font-size: 4rem;
margin-top: 4rem;
line-height: 132%;
}
}
> .image {
margin: 4.9rem auto 9.5rem;
width: 65.3rem;
height: 86.5rem;
border-radius: 1rem;
backdrop-filter: blur(5.27rem);
-webkit-backdrop-filter: blur(5.27rem);
-moz-backdrop-filter: blur(5.27rem);
-ms-backdrop-filter: blur(5.27rem);
-o-backdrop-filter: blur(5.27rem);
box-shadow: 1.9rem 2.3rem 1.66rem 0.23rem -0.3rem 0.23rem #36180c40;
border: 0.439rem solid #fff;
// border-image: linear-gradient(90deg,#BF926E94, #ffffff) 1;
display: flex;
align-items: center;
justify-content: center;
> img {
width: 58.9rem;
height: 79.2rem;
border-radius: 1rem;
border: 0.2rem solid #d9d9d9;
object-fit: cover;
}
}
> button {
width: 40rem;
height: 8.3rem;
border-radius: 0.7rem;
}
}
}
</style>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import HeaderTitle from '@/components/HeaderTitle.vue' import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue' import FooterNavigation from '@/components/FooterNavigation.vue'
import { ref, onMounted } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useGenerateStore } from '@/stores' import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore() const generateStore = useGenerateStore()
const emit = defineEmits(['view-type']) const emit = defineEmits(['view-type'])
@@ -10,22 +10,26 @@
emit('view-type', 1) emit('view-type', 1)
}) })
const router = useRouter() const router = useRouter()
const faceUrl = ref('') const route = useRoute()
const query = computed(() => route.query)
const isDemo = computed(() => route.query.demo === '1')
// 上传照片 // 上传照片
const handleUploadFace = () => { const handleUploadFace = () => {
// generateStore.updatePhotoInfo({}) // generateStore.updatePhotoInfo({})
router.push({ name: 'uploadFace2' }) router.push({ name: 'uploadFace2', query: query.value })
} }
// 跳过上传 // 跳过上传
const handleFinish = () => { const handleFinish = () => {
generateStore.updatePhotoInfo({}) generateStore.updatePhotoInfo({})
generateStore.clearCustomizeInfo() if (!isDemo.value) {
generateStore.uploadCustomizeInfo({ generateStore.clearCustomizeInfo()
tryOnId: generateStore.originalTryOn.id, generateStore.uploadCustomizeInfo({
tryOnUrl: generateStore.originalTryOn.tryOnUrl, tryOnId: generateStore.originalTryOn.id,
isFavorite: generateStore.originalTryOn.isLike tryOnUrl: generateStore.originalTryOn.tryOnUrl,
}) isFavorite: generateStore.originalTryOn.isLike
router.push({ name: 'customize' }) })
}
router.push({ name: 'customize', query: query.value })
} }
</script> </script>

View File

@@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import HeaderTitle from '@/components/HeaderTitle.vue' import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue' import FooterNavigation from '@/components/FooterNavigation.vue'
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { uploadCustomerPhoto } from '@/api/workshop' import { uploadCustomerPhoto } from '@/api/workshop'
import { useGenerateStore } from '@/stores' import { useGenerateStore } from '@/stores'
const generateStore = useGenerateStore() const generateStore = useGenerateStore()
@@ -12,6 +12,9 @@
emit('view-type', 1) emit('view-type', 1)
}) })
const router = useRouter() const router = useRouter()
const route = useRoute()
const query = computed(() => route.query)
const isDemo = computed(() => route.query.demo === '1')
const fileData = generateStore.photoInfo const fileData = generateStore.photoInfo
if (!fileData.file?.size) generateStore.updatePhotoInfo({}) if (!fileData.file?.size) generateStore.updatePhotoInfo({})
// 上传照片 // 上传照片
@@ -37,7 +40,7 @@
} }
// 生成照片 // 生成照片
const handleGenerate = () => { const handleGenerate = () => {
if (fileData.id) return router.push({ name: 'customize' }) if (fileData.id) return router.push({ name: 'customize', query: query.value })
if (!fileData.file) return if (!fileData.file) return
const formData = new FormData() const formData = new FormData()
formData.append('customerId', generateStore.customerId + '') formData.append('customerId', generateStore.customerId + '')
@@ -45,8 +48,8 @@
formData.append('file', fileData.file) formData.append('file', fileData.file)
uploadCustomerPhoto(formData).then((res) => { uploadCustomerPhoto(formData).then((res) => {
generateStore.updatePhotoInfo({ ...res, file: fileData.file }) generateStore.updatePhotoInfo({ ...res, file: fileData.file })
generateStore.clearCustomizeInfo() isDemo.value ? generateStore.clearCustomizeInfoDemo() : generateStore.clearCustomizeInfo()
router.push({ name: 'customize' }) router.push({ name: 'customize', query: query.value })
}) })
} }
// 处理照片加载错误 // 处理照片加载错误

View File

@@ -6,8 +6,8 @@
v-for="(line, index) in lines" v-for="(line, index) in lines"
:key="index" :key="index"
:class="['line', `line-${line.type}`]" :class="['line', `line-${line.type}`]"
></div> </template ></div>
>> </template>
</div> </div>
</div> </div>
</template> </template>
@@ -44,7 +44,8 @@ const calculateLines = (): Line[] => {
// 这样当滚动到50%时,内容看起来和开始一样 // 这样当滚动到50%时,内容看起来和开始一样
const availableWidth = containerWidth const availableWidth = containerWidth
const lineWithGap = lineWidthPx + gapPx const lineWithGap = lineWidthPx + gapPx
const linesNeeded = Math.ceil(availableWidth / lineWithGap) // 使用四舍五入让一个周期的宽度尽量接近容器宽度,减少滚动结束时的“回跳”感
const linesNeeded = Math.max(1, Math.round(availableWidth / lineWithGap))
const generatedLines: Line[] = [] const generatedLines: Line[] = []

View File

@@ -86,7 +86,7 @@ const shortcutList: string[] = [
const handleSend = (): void => { const handleSend = (): void => {
if (inputValue.value.trim()) { if (inputValue.value.trim()) {
console.log('input发送消息:', inputValue.value) // console.log('input发送消息:', inputValue.value)
emit('send-message', inputValue.value) emit('send-message', inputValue.value)
inputValue.value = '' inputValue.value = ''
// 重置textarea高度 // 重置textarea高度

View File

@@ -30,13 +30,14 @@ import NoticeList from './components/NoticeList.vue'
import InputArea from './components/InputArea.vue' import InputArea from './components/InputArea.vue'
import GenerateLoading from './components/GenerateLoading.vue' import GenerateLoading from './components/GenerateLoading.vue'
import { ref, onMounted, onUnmounted, onActivated } from 'vue' import { ref, onMounted, onUnmounted, onActivated } from 'vue'
import { useRouter } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { useUserInfoStore, useGenerateStore } from '@/stores' import { useUserInfoStore, useGenerateStore } from '@/stores'
import { streamChatAddress } from '@/api/workshop' import { streamChatAddress } from '@/api/workshop'
import { generateUUID } from '@/utils/tools' import { generateUUID } from '@/utils/tools'
import { showToast } from 'vant' import { showToast } from 'vant'
const router = useRouter() const router = useRouter()
const route = useRoute()
const generateStore = useGenerateStore() const generateStore = useGenerateStore()
const userInfoStore = useUserInfoStore() const userInfoStore = useUserInfoStore()
@@ -68,9 +69,21 @@ const isStreaming = ref<boolean>(false)
const currentStreamingMessage = ref<ChatMessage | null>(null) const currentStreamingMessage = ref<ChatMessage | null>(null)
const sessionId = ref<string>('') const sessionId = ref<string>('')
const sendPrefilledMessage = () => {
const { message, ...restQuery } = route.query
if (typeof message === 'string' && message.trim()) {
handleSendMessage(message)
router.replace({
path: route.path,
query: restQuery
})
}
}
onMounted(() => { onMounted(() => {
sessionId.value = Math.floor(Date.now() / 1000).toString() sessionId.value = Math.floor(Date.now() / 1000).toString()
generateStore.setSessionId(sessionId.value) generateStore.setSessionId(sessionId.value)
sendPrefilledMessage()
}) })
onActivated(() => { onActivated(() => {

View File

@@ -25,15 +25,15 @@
<div class="glass-form"> <div class="glass-form">
<div class="form-field"> <div class="form-field">
<label class="field-label">Customer Name</label> <label class="field-label">VIP ID</label>
<input v-model="customerData.name" type="text" placeholder="Name" class="form-input" /> <input v-model="customerData.name" type="text" placeholder="Enter your ID" class="form-input" />
</div> </div>
<div class="form-field email"> <div class="form-field email">
<label class="field-label">Customer Email</label> <label class="field-label">Email Address</label>
<input <input
v-model="customerData.email" v-model="customerData.email"
type="email" type="email"
placeholder="Email" placeholder="Enter your email"
class="form-input" class="form-input"
/> />
</div> </div>
@@ -78,13 +78,13 @@ const handleConfirm = async () => {
}) })
return return
} }
console.log('customerData.value', customerData.value)
customerCheckin(customerData.value).then((res) => { customerCheckin(customerData.value).then((res) => {
useUserInfoStore().resetGenerateParams() useUserInfoStore().resetGenerateParams()
// console.log('res', res) // console.log('res', res)
generateStore.setCustomerInfo(res) generateStore.setCustomerInfo(res)
router.push('/stylist/index') // router.push('/stylist/index')
router.push('/homeNav')
}) })
} }
</script> </script>

View File

@@ -7,25 +7,164 @@
<SvgIcon name="setting" size="70" /> <SvgIcon name="setting" size="70" />
</div> --> </div> -->
<div class="text">What are you dressing for?</div> <div class="text">What are you dressing for?</div>
<div class="start-btn" @click="handleStart">Start</div> <!-- <div class="start-btn" @click="handleStart">Start</div> -->
<div class="chatbox flex flex-center">
<div class="input-box flex">
<div class="input-wrapper flex-1 flex">
<input
type="text"
class="input-item flex-1"
v-model="inputValue"
placeholder="Ask sth!"
v-show="!isRecording"
/>
<div class="recording-visualizer flex-1" v-show="isRecording">
<AudioVisualizer ref="audioVisualizerRef" />
</div>
</div>
<SvgIcon
class="audio-icon"
:name="isRecording ? 'pause' : 'audio'"
size="52"
@click="handleClickAudio"
/>
</div>
<div class="send flex flex-center" @click="handleSendMessage">
<SvgIcon class="send-icon" name="send" size="26" color="#000000" />
</div>
</div>
</div> </div>
</div> </div>
<footer-navigation /> <footer-navigation />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onUnmounted, nextTick, watch } from 'vue'
import { showToast } from 'vant'
import HeaderTitle from '@/components/HeaderTitle.vue' import HeaderTitle from '@/components/HeaderTitle.vue'
import FooterNavigation from '@/components/FooterNavigation.vue' import FooterNavigation from '@/components/FooterNavigation.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import AudioVisualizer from '@/views/asistant/components/AudioVisualizer.vue'
const router = useRouter() const router = useRouter()
const handleBack = () => { const inputValue = ref('')
router.go(-1) const isRecording = ref(false)
const audioVisualizerRef = ref<InstanceType<typeof AudioVisualizer> | null>(null)
let speechRecognition: any = null
let lastTranscript = ''
let isSpeechRecognitionActive = false
const refreshAudioVisualizer = () => {
audioVisualizerRef.value?.updateLines?.()
} }
const handleStart = () => { watch(isRecording, async (newVal) => {
console.log('click start') if (newVal) {
router.push('/asistant') await nextTick()
refreshAudioVisualizer()
setTimeout(() => {
refreshAudioVisualizer()
}, 50)
}
})
const handleSendMessage = () => {
const message = inputValue.value.trim()
if(!message){
showToast('Please enter a message')
return
}
router.push({
path: '/asistant',
query: message ? { message } : undefined
})
} }
const handleClickAudio = () => {
if (isRecording.value) {
stopRecording()
} else {
startRecording()
}
}
const startRecording = () => {
if (isSpeechRecognitionActive) {
console.warn('Speech recognition already running')
return
}
if (!speechRecognition) {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
showToast(
'Your browser does not support speech recognition, please try again with another browser'
)
return
}
const SpeechRecognition =
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
speechRecognition = new SpeechRecognition()
speechRecognition.continuous = true
speechRecognition.interimResults = true
speechRecognition.lang = 'en-US'
}
speechRecognition.onstart = () => {
isRecording.value = true
}
speechRecognition.onresult = (event: any) => {
let finalTranscript = ''
let interimTranscript = ''
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript
if (event.results[i].isFinal) {
finalTranscript += transcript
} else {
interimTranscript += transcript
}
}
if (finalTranscript && finalTranscript !== lastTranscript) {
lastTranscript = finalTranscript
inputValue.value = finalTranscript
}
if (interimTranscript) {
console.log('Speech recognition interim result:', interimTranscript)
}
}
speechRecognition.onend = () => {
isRecording.value = false
lastTranscript = ''
isSpeechRecognitionActive = false
}
speechRecognition.onerror = (event: any) => {
console.error('Speech recognition error:', event.error)
isRecording.value = false
isSpeechRecognitionActive = false
showToast('Speech recognition failed, please try again')
}
speechRecognition.start()
isSpeechRecognitionActive = true
}
const stopRecording = () => {
if (speechRecognition && isSpeechRecognitionActive) {
speechRecognition.stop()
isSpeechRecognitionActive = false
}
}
onUnmounted(() => {
if (speechRecognition && isRecording.value) {
speechRecognition.stop()
}
speechRecognition = null
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.dressfor-container { .dressfor-container {
@@ -54,16 +193,52 @@ const handleStart = () => {
margin-top: 43.8rem; margin-top: 43.8rem;
margin-bottom: 14rem; margin-bottom: 14rem;
} }
.start-btn { .chatbox {
font-size: 5.6rem; height: 9.3rem;
width: 32.5rem; // background-color: #fff;
height: 8.1rem; column-gap: 2.29rem;
border: .2rem solid #fff; .input-box {
display: flex; width: 59.8rem;
align-items: center; height: 100%;
justify-content: center; background-color: #fff;
border-radius: 4rem; border: 2px solid #5f5f5f;
margin: 0 auto; border-radius: 1rem;
color: #222222;
font-size: 3.2rem;
font-family: 'satoshiRegular';
padding: 0 2.6rem;
column-gap: 2.6rem;
overflow: hidden;
.input-wrapper{
overflow: hidden;
}
.recording-visualizer {
display: flex;
align-items: center;
height: 100%;
:deep(.audio-visualizer) {
width: 100%;
padding: 0;
}
:deep(.visualizer-container) {
height: 100%;
}
}
.input-item {
// width: 100%;
height: 100%;
outline: none;
border: none;
}
.audio-icon {
width: initial;
}
}
.send {
width: 7.6rem;
height: 7.6rem;
background-color: #fff;
}
} }
} }
} }