Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

sys5923812@126.com 1 день назад
Родитель
Сommit
0ab2edb06f

BIN
src/assets/images/environment/ai-career-background.png


BIN
src/assets/images/environment/ai-career-chat-profile.png


BIN
src/assets/images/environment/ai-career-msg-expand.png


BIN
src/assets/images/environment/ai-career-msg-fold.png


BIN
src/assets/images/environment/ai-career-profile.png


+ 121 - 81
src/views/xjc-integratedmachine/environment/ai_interview/ai_career_interview.vue

@@ -10,7 +10,7 @@
                                     v-model="form.name"
                                     style="width: 1484px;height: 84px"
                                     placeholder="请输入你要访谈的职业"
-                                    @change="byKeyword"
+                                    @input="byKeyword"
                             >
                                 <template #suffix>
                                     <span style="font-size: 38px;color: #444040;margin-left: 37px;">
@@ -18,23 +18,24 @@
                                     </span>
                                 </template>
                             </el-input>
-                            <div>
+                            <div @click="byKeyword">
                                 开始查询
                             </div>
-<!--                            <img @click="search" style="z-index:10" src="@/assets/images/environment/search-btn.png">-->
                         </div>
- <!--                       <div class="result-box">
-                            <div class="two-page-result">
-                                <div v-for="(item,index) in universityList"
-                                     :class="[item.selected?'item-result-box-active':'item-result-box']"
-                                     @click="toDetail(item)">
-                                    <div v-html="item.name"></div>
+                    </div>
+                    <div style="overflow: auto;height: 720px;margin-top: 16px">
+                        <div style="display: flex; flex-direction: column; align-items: center;">
+                            <div class="result-box" v-if="occupationList.length>0">
+                                <div class="two-page-result">
+                                    <div v-for="(item,index) in occupationList"
+                                         :class="[item.selected?'item-result-box-active':'item-result-box']"
+                                         @click="toDetail(item)">
+                                        <div v-html="item.name"></div>
+                                    </div>
                                 </div>
                             </div>
-                        </div>-->
+                        </div>
 
-                    </div>
-                    <div style="overflow: auto;height: 720px;margin-top: 16px">
                         <div class="hot-search">
                             <div class="title">热门搜索:</div>
                             <div style="display: flex;flex-wrap:wrap;
@@ -93,17 +94,17 @@
     import headComponent from '@/views/xjc-integratedmachine/components/head_component.vue'
     import Drag_component from "@/views/xjc-integratedmachine/components/drag_component.vue";
     const headinfo = ref({})
+    import {getOccupationByKeyword} from "@/api/xjc-integratedmachine/environment/occupation.js";
+
+    const router = useRouter()
 
     const form = ref({
-        areaid: null,
-        areaname: null,
-        educationlevel: null,
-        educationlevelName: null,
-        characteristic: null,
-        characteristicName: null,
         name: null
     })
 
+    // 职业列表
+    const occupationList = ref([])
+
     function setHeadinfo(){
         headinfo.value = {
             title: 'AI生涯访谈',
@@ -115,6 +116,46 @@
         }
     }
 
+    // 定义一个函数,用于将关键词变为红色
+    function highlightKeywords(text, keywords) {
+        // 遍历关键词列表
+        keywords.forEach(keyword => {
+            // 使用正则表达式匹配关键词,并替换为带有红色样式的HTML标签
+            // 这里使用全局匹配标志'g',以确保替换所有出现的关键词
+            // console.log("keywords", keywords)
+            let regex = new RegExp(keyword, 'g');
+            text = text.replace(regex, `<span style="color: #0DE6A1;">${keyword}</span>`);
+            // console.log("text", text)
+        });
+        return text;
+    }
+
+    function byKeyword() {
+        if(form.value.name){
+            getOccupationByKeyword(form.value).then(resp => {
+                resp.list.map(item => {
+                    item.selected = false;
+                    let keyWord = form.value.name
+                    item.name = highlightKeywords(item.name, [keyWord]);
+                })
+
+                occupationList.value = resp.list
+            })
+        }else{
+            occupationList.value = []
+        }
+    }
+
+    function toDetail(row) {
+        router.push({
+            path: '/xjc-integratedmachine/environment/ai_career_interview_chat',
+            query: {
+                id: row.id
+            }
+        })
+    }
+
+
     onMounted(() => {
         setHeadinfo()
     })
@@ -195,74 +236,73 @@
                 justify-content: center;
             }
         }
+    }
 
-        .result-box {
-            width: 1484px;
-            height: 750px;
-            overflow: auto;
+    .result-box {
+        width: 1484px;
+        /*height: 750px;*/
+        /*overflow: auto;*/
 
-            .two-page-result {
-                display: flex;
-                flex-wrap: wrap;
-                justify-content: space-between;
-                align-items: center;
+        .two-page-result {
+            display: flex;
+            flex-wrap: wrap;
+            /*justify-content: space-between;*/
+            align-items: center;
+
+            .item-result-box-active {
+                min-width: 320px;
+                height: 92px;
+                background: linear-gradient(180deg, #B6FFEF 0%, #C5EEFF 100%);
+                box-shadow: inset 0px -2px 7px 0px #1E410E;
+                border-radius: 5px 5px 5px 5px;
+                border: 1px solid #A2F57F;
+                font-weight: 400;
+                font-size: 30px;
+                color: #0DE6A1;
+                line-height: 90px;
+                text-align: center;
+                margin-left: 50px;
+                margin-right: 50px;
+                margin-top: 32px;
+                padding-left: 20px;
+                padding-right: 20px;
+            }
 
-                .item-result-box-active {
-                    min-width: 320px;
-                    height: 92px;
-                    background: linear-gradient(180deg, #B6FFEF 0%, #C5EEFF 100%);
-                    box-shadow: inset 0px -2px 7px 0px #1E410E;
-                    border-radius: 5px 5px 5px 5px;
-                    border: 1px solid #A2F57F;
-                    font-weight: 400;
-                    font-size: 30px;
-                    color: #0DE6A1;
-                    line-height: 90px;
-                    text-align: center;
-                    margin-left: 50px;
-                    margin-right: 50px;
-                    margin-top: 32px;
-                    padding-left: 20px;
-                    padding-right: 20px;
-                }
-
-                .item-result-box {
-                    min-width: 320px;
-                    height: 92px;
-                    background: #E0EEF4;
-                    border-radius: 5px 5px 5px 5px;
-                    font-weight: 400;
-                    font-size: 30px;
-                    color: #000000;
-                    line-height: 90px;
-                    text-align: center;
-                    margin-left: 50px;
-                    margin-right: 50px;
-                    margin-top: 32px;
-                    padding-left: 20px;
-                    padding-right: 20px;
-                }
-
-                .item-result-box:hover {
-                    min-width: 320px;
-                    height: 92px;
-                    background: linear-gradient(180deg, #B6FFEF 0%, #C5EEFF 100%);
-                    box-shadow: inset 0px -2px 7px 0px #1E410E;
-                    border-radius: 5px 5px 5px 5px;
-                    border: 1px solid #A2F57F;
-                    font-weight: 400;
-                    font-size: 30px;
-                    color: #000000;
-                    line-height: 90px;
-                    text-align: center;
-                    margin-left: 50px;
-                    margin-top: 32px;
-                    padding-left: 20px;
-                    padding-right: 20px;
-                }
+            .item-result-box {
+                min-width: 320px;
+                height: 92px;
+                background: #E0EEF4;
+                border-radius: 5px 5px 5px 5px;
+                font-weight: 400;
+                font-size: 30px;
+                color: #000000;
+                line-height: 90px;
+                text-align: center;
+                margin-left: 50px;
+                margin-right: 50px;
+                margin-top: 32px;
+                padding-left: 20px;
+                padding-right: 20px;
             }
-        }
 
+            .item-result-box:hover {
+                min-width: 320px;
+                height: 92px;
+                background: linear-gradient(180deg, #B6FFEF 0%, #C5EEFF 100%);
+                box-shadow: inset 0px -2px 7px 0px #1E410E;
+                border-radius: 5px 5px 5px 5px;
+                border: 1px solid #A2F57F;
+                font-weight: 400;
+                font-size: 30px;
+                color: #000000;
+                line-height: 90px;
+                text-align: center;
+                margin-left: 50px;
+                margin-top: 32px;
+                padding-left: 20px;
+                padding-right: 20px;
+            }
+        }
     }
 
     .hot-search {

+ 896 - 2
src/views/xjc-integratedmachine/environment/ai_interview/ai_career_interview_chat.vue

@@ -1,11 +1,905 @@
 <template>
-    AI生涯访谈2
+    <div class="ai_career_interview_chat">
+        <div class="left-box">
+            <div class="profile">
+                <img  src="@/assets/images/environment/ai-career-profile.png" alt="404">
+            </div>
+            <div class="introduce-text">
+                在生涯访谈中,通过与不同职业人物对话,可以了解不同职业的发展前景、职业要求,为尽早规划职业发展 xxxx
+                <p>访谈时你可以提出以下问题:</p>
+            </div>
+            <div class="question-list">
+                <p class="question-list-item">您为什么要选择这一职业?</p>
+                <p class="question-list-item">这项职业每天的核心任务是什么?</p>
+                <p class="question-list-item">这项职业的工作环境如何?(室内/室外,办公室/工厂,团队工作/独自工作等)</p>
+                <p class="question-list-item">从事这项职业需要哪些学历、证书、经验或关键能力?</p>
+                <p class="question-list-item">这项职业在招聘或晋升时,更看重哪些个人特质或阶段特征?</p>
+                <p class="question-list-item">大学里哪些专业与这项职业直接相关?</p>
+                <p class="question-list-item">为了从事这项职业,在校期间应做哪些具体准备?</p>
+                <p class="question-list-item">请分享一个最有成就感的项目/瞬间,以及一次印象深刻的挫折,您从中学到的关键经验是什么?</p>
+                <p class="question-list-item">这项职业未来的发展前景如何?</p>
+                <p class="question-list-item">这项职业进一步提升的途径有哪些?(培训、证书、项目、学历等)</p>
+                <p class="question-list-item">如果未来的我将选择从事该职业,您会给我哪些具体建议?</p>
+                <p class="question-list-item">请用时间轴描述一个典型工作日从上班到下班的详细流程?</p>
+            </div>
+
+        </div>
+
+        <div class="center-box">
+            <div class="center-header">
+                <div style="margin-left: 30px"></div>
+                <div>小新老师</div>
+                <div class="header-exit-btn">退出</div>
+            </div>
+            <div v-loading="loadingHistoryRecord" class="chat-container">
+                <div class="message-list" ref="chatContainerRef">
+                    <div v-for="(item, index) in chatRecordList" :key="item.id">
+                        <div v-if="item.isUser == 1" class="user-message">
+                            <img src="@/assets/images/environment/ai-career-chat-profile.png" alt="icon" style="margin-left: 4px;height: 50px;width: 50px"/>
+                            <span class="user-message-content content-text" >{{item.content}}</span>
+                        </div>
+                        <div v-else class="bot-message">
+                            <img src="@/assets/images/environment/ai-career-chat-profile.png" alt="icon" style="margin-left: 4px;height: 50px;width: 50px"/>
+                            <div style="margin-left: 4px;">
+                                <div class="bot-message-content">
+                                    <div v-if="index === chatRecordList.length-1">
+                                        <div v-if="item.isTyping">
+                                            <div v-html="item.content" class="content-text"></div>
+                                            <span class="loading-dots" style="display: flex;justify-content: center">
+                                                <span class="dot"></span>
+                                                <span class="dot"></span>
+                                            </span>
+                                        </div>
+                                        <div v-else>
+                                            <div v-html="item.content"  class="content-text"></div>
+                                        </div>
+                                    </div>
+                                    <div v-else>
+                                        <div v-show="!expandIndexList.includes(index)" v-html="item.content.substring(0, 80)"  class="content-text"></div>
+                                        <div v-show="expandIndexList.includes(index)" v-html="item.content"  class="content-text"></div>
+                                    </div>
+                                </div>
+                                <div style="margin: 4px">
+                                    <el-text>{{item.createTime}}</el-text>
+                                </div>
+                            </div>
+                            <div v-if="index !== chatRecordList.length-1">
+                               <!-- <el-button v-if="item.content.length>80" style="margin-left: 4px"
+                                           @click="foldOrExpandMessage(index, expandIndexList.includes(index))">
+                                    {{expandIndexList.includes(index)?'折叠':'展开'}}
+                                </el-button>-->
+                                <img v-if="item.content.length>80"
+                                     @click="foldOrExpandMessage(index, expandIndexList.includes(index))"
+                                    :src="expandIndexList.includes(index)?msgFold:msgExpand"
+                                    alt="icon"
+                                    style="margin-left: 4px;height: 36px;width: 36px"/>
+                            </div>
+                            <el-button @click="ttsStartPlay(item.content, index)">{{playActiveIndex === index && playButtonFlag ?'停止':'播放'}}</el-button>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="input-container">
+                <el-input
+                        v-model="inputMessage"
+                        type="textarea"
+                        placeholder="请输入消息"
+                        @keyup.enter="sendMessage"
+                ></el-input>
+                <el-button @click="sendMessage" :disabled="isSending" type="primary">发送</el-button>
+            </div>
+        </div>
+    </div>
 </template>
 
 <script setup>
+    import headComponent from '@/views/xjc-integratedmachine/components/head_component.vue'
+    import Drag_component from "@/views/xjc-integratedmachine/components/drag_component.vue";
+    import {onMounted, ref, nextTick} from "vue";
+    import { getToken } from '@/utils/auth'
+    // md转换为html
+    import { marked } from 'marked'
+    // 语音识别
+    import CryptoJS from 'crypto-js';
+    import * as RecorderManager from "/public/ai/iat/dist/index.umd.js"
+    // 语音播放
+    import {Base64} from 'js-base64'
+    import * as AudioPlayer from "/public/ai/tts/dist/index.umd.js"
+    import {aiChatRecordList, aiChatRecordAdd} from '@/api/xjc-integratedmachine/common/aiChat.js'
+    import msgFold from '@/assets/images/environment/ai-career-msg-fold.png'
+    import msgExpand from '@/assets/images/environment/ai-career-msg-expand.png'
+    const router = useRouter()
+    const route = useRoute()
+
+    const param = route.query
+
+
+    const loadingHistoryRecord = ref(false)
+    // 聊天记录
+    let chatRecordList = ref([])
+    // md流式数据
+    const streamMarkdownData = ref('');
+    // html流式数据
+    let streamHtmlData = ref('');
+    // 流式加载状态
+    const isLoading = ref(false);
+    // 读取器实例
+    let reader = null;
+    // AbortController用于中止请求
+    let controller = null;
+    // 输入的问题
+    let inputMessage = ref('')
+    // 发送标识
+    let isSending = ref(false)
+    // 选中的消息,用于展开折叠
+    let expandIndexList = ref([])
+    // 播放按钮状态
+    let playActiveIndex = ref(0)
+    let playButtonFlag = ref(false)
+    // 查看所有聊天记录
+    function list() {
+        loadingHistoryRecord.value = true
+        let queryForm = {
+            pageNum: 1,
+            pageSize: 10000
+        }
+        aiChatRecordList(queryForm).then(resp =>{
+            chatRecordList.value = resp.rows;
+            loadingHistoryRecord.value = false
+            setTimeout(()=>{
+                scrollToBottom();
+            }, 100);
+            if(resp.total == 0){
+                sayHi();
+            }
+        })
+    }
+
+    function addRecord(content) {
+        let queryForm = {
+            content: content,
+            isUser: 0
+        }
+        aiChatRecordAdd(queryForm).then(resp =>{
+            console.log(resp)
+
+        })
+    }
+
+    function sayHi() {
+        const botMsg = {
+            isUser: false,
+            content: '你好,我是来帮助你进行学习规划、选科辅导以及志愿填报的生涯教育专家。如果你有关于这些方面的问题,欢迎随时向我咨询!', // 增量填充
+            isTyping: false, // 显示加载动画
+        }
+        chatRecordList.value.push(botMsg)
+    }
+
+    const sendMessage = () => {
+        if (inputMessage.value.trim()) {
+            sendRequest(inputMessage.value.trim())
+            inputMessage.value = ''
+        }
+    }
+
+    const sendRequest = async(message) => {
+        // 用户信息
+        const userMessage = {
+            isUser: true,
+            content: message,
+            isTyping: false
+        }
+        // 消息加入聊天记录
+        chatRecordList.value.push(userMessage)
+        const botMsg = {
+            isUser: false,
+            content: '', // 增量填充
+            isTyping: true, // 显示加载动画
+        }
+        chatRecordList.value.push(botMsg)
+        const lastMsg = chatRecordList.value[chatRecordList.value.length - 1]
+
+
+        try{
+            isLoading.value = true;
+            streamMarkdownData.value = ''; // 清空之前的数据
+
+            // 创建AbortController以便可以中止请求
+            controller = new AbortController();
+
+            // 请求体
+            let form = {
+                "content": message? message: "你是谁?"
+            }
+            // 发送fetch请求
+            const response = await fetch('/dev-api/ai/chat/record/stream', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/json',
+                    'Authorization': 'Bearer ' + getToken()
+                },
+                body: JSON.stringify(form)
+            });
+
+            // 获取可读流的读取器
+            reader = response.body.getReader();
+            const decoder = new TextDecoder('utf-8');
+
+            // 循环读取流数据
+            while (true) {
+                const { done, value } = await reader.read();
+                if (done) break; // 如果流读取完成则退出循环
+
+                // 解码并追加数据
+                let chunk = decoder.decode(value, { stream: true });
+                chunk = chunk.replace(/\n\n/g, '').replace(/data:/g, '')
+                streamMarkdownData.value += chunk;
+                streamHtmlData.value = marked(streamMarkdownData.value)
+                lastMsg.content = marked(streamMarkdownData.value)
+            }
+        }catch (error) {
+            // 如果是手动中止,不显示错误
+            if (error.name !== 'AbortError') {
+                console.error('流式读取失败:', error);
+                streamHtmlData.value = 'Error: ' + error.message;
+            }
+        } finally {
+            isLoading.value = false;
+            let htmlData = marked(streamMarkdownData.value);
+            addRecord(htmlData);
+            streamHtmlData.value = htmlData;
+            lastMsg.content = marked(streamMarkdownData.value)
+            lastMsg.isTyping = false
+            // 组件挂载后自动滚动到底部
+            scrollToBottom();
+        }
+    }
+
+    const stopMessage = () => {
+        if (reader) {
+            // 取消读取
+            reader.cancel().catch(() => {});
+            reader = null;
+        }
+        if (controller) {
+            // 中止请求
+            controller.abort();
+            controller = null;
+        }
+        isLoading.value = false;
+    }
+
+    function foldOrExpandMessage(index, flag){
+        if(flag){
+            expandIndexList.value = expandIndexList.value.filter((item)=>{
+                return item !== index;
+            })
+        }else{
+            expandIndexList.value.push(index)
+        }
+    }
+
+    /* 语音识别 ↓*/
+    // 控制录音弹窗
+    let iatVoiceOpen = false
+    // 是否开始录音
+    let iatStartVoiceStatus = false
+    // 识别中状态
+    let iatIdentifyStatus = false
+    // 录音对象
+    let iatRecorder = null
+    let iatTranscription = ''
+    let iatBtnStatus = ''
+    let iatResultText = ''
+    let iatResultTextTemp = ''
+    let iatCountdownInterval = null
+    let iatWS = null
+    let iatRecognition = null
+    let iatButtonDisabled = true
+    let iatLoading = false
+    let xfIatKeys = {
+        APPID: '5a2643f4',
+        APISecret: 'MTg4MWIzY2VmYTg2YTEwMjliMDY1N2Iz',
+        APIKey: '8b1a53486bec887eb817b4410aa743ed',
+    }
+    // 初始化语音识别
+    function initSpeechRecognition(){
+        // 初始化录音
+        iatRecorder = new window.RecorderManager('/ai/iat/dist');
+        // 开始录音
+        iatRecorder.onStart = () => {
+            iatChangeBtnStatus('OPEN');
+        };
+        iatRecorder.onFrameRecorded = ({ isLastFrame, frameBuffer }) => {
+            if (iatWS.readyState === iatWS.OPEN) {
+                iatWS.send(
+                    JSON.stringify({
+                        data: {
+                            status: isLastFrame ? 2 : 1,
+                            format: 'audio/L16;rate=16000',
+                            encoding: 'raw',
+                            audio: toBase64(frameBuffer),
+                        },
+                    }),
+                );
+                if (isLastFrame) {
+                    iatChangeBtnStatus('CLOSING');
+                }
+            }
+        };
+        iatRecorder.onStop = () => {
+            console.log('录音结束,停止定时器');
+            clearInterval(iatCountdownInterval);
+            iatStartVoiceStatus = false;
+        };
+
+    }
+
+    async function iatStartVoice() {
+        if (iatLoading) {
+            return;
+        }
+        iatVoiceOpen = true;
+        await iatPlayIatVoice();
+    }
+
+    async function iatPlayIatVoice() {
+        if (iatLoading) {
+            return;
+        }
+        iatStartVoiceStatus = !iatStartVoiceStatus;
+        // 浏览器自带的识别
+        if (iatRecognition) {
+            if (iatStartVoiceStatus) {
+                iatRecognition.start();
+            } else {
+                iatRecognition.stop();
+            }
+            return;
+        }
+        if (iatStartVoiceStatus) {
+            iatConnectWebSocket();
+        } else {
+            iatRecorder.stop();
+        }
+    }
+
+    /**
+     * 关闭录音弹窗
+     */
+    function iatCloseVoiceOpen() {
+        iatVoiceOpen = false;
+        iatStartVoiceStatus = false;
+        if (iatRecorder) {
+            iatRecorder.stop();
+        }
+        if (iatRecognition) {
+            iatRecognition.stop();
+        }
+        iatTranscription = '';
+    }
+
+    function iatRenderResult(resultData) {
+        // 识别结束
+        const jsonData = JSON.parse(resultData);
+        if (jsonData.data && jsonData.data.result) {
+            const data = jsonData.data.result;
+            let str = '';
+            const { ws } = data;
+            for (let i = 0; i < ws.length; i += 1) {
+                str += ws[i].cw[0].w;
+            }
+            // 开启wpgs会有此字段(前提:在控制台开通动态修正功能)
+            // 取值为 "apd"时表示该片结果是追加到前面的最终结果;取值为"rpl" 时表示替换前面的部分结果,替换范围为rg字段
+            if (data.pgs) {
+                if (data.pgs === 'apd') {
+                    // 将resultTextTemp同步给resultText
+                    iatResultText = iatResultTextTemp;
+                }
+                // 将结果存储在resultTextTemp中
+                iatResultTextTemp = iatResultText + str;
+            } else {
+                iatResultText += str;
+            }
+            inputMessage.value = iatResultTextTemp || iatResultText || '';
+        }
+        if (jsonData.code === 0 && jsonData.data.status === 2) {
+            iatWS.close();
+        }
+        if (jsonData.code !== 0) {
+            iatWS.close();
+            console.error(jsonData);
+        }
+    }
+
+    function iatConnectWebSocket() {
+        const websocketUrl = iatGetWebSocketUrl();
+        if ('WebSocket' in window) {
+            iatWS = new window.WebSocket(websocketUrl);
+        } else if ('MozWebSocket' in window) {
+            iatWS = new window.MozWebSocket(websocketUrl);
+        } else {
+            message.error('浏览器不支持WebSocket');
+            return;
+        }
+        iatChangeBtnStatus('CONNECTING');
+        iatWS.onopen = e => {
+            console.log('iatWS.onopen', e);
+            // 开始录音
+            iatRecorder.start({
+                sampleRate: 16000,
+                frameSize: 1280,
+            });
+            const params = {
+                common: {
+                    app_id: xfIatKeys.APPID,
+                },
+                business: {
+                    language: 'zh_cn',
+                    domain: 'iat',
+                    accent: 'mandarin',
+                    vad_eos: 5000,
+                    dwa: 'wpgs',
+                    nbest: 1,
+                    wbest: 1,
+                },
+                data: {
+                    status: 0,
+                    format: 'audio/L16;rate=16000',
+                    encoding: 'raw',
+                },
+            };
+            iatWS.send(JSON.stringify(params));
+        };
+        iatWS.onmessage = e => {
+            iatRenderResult(e.data);
+        };
+        iatWS.onerror = e => {
+            console.error(e);
+            iatRecorder.stop();
+            iatChangeBtnStatus('CLOSED');
+        };
+        iatWS.onclose = e => {
+            console.log(e);
+            iatRecorder.stop();
+            iatChangeBtnStatus('CLOSED');
+        };
+    }
+
+    function iatGetWebSocketUrl() {
+        const { APIKey, APISecret } = xfIatKeys;
+        if (!APIKey) {
+            message.error('语音识别配置未生效');
+            return null;
+        }
+        // 请求地址根据语种不同变化
+        let url = 'wss://iat-api.xfyun.cn/v2/iat';
+        const host = 'iat-api.xfyun.cn';
+        const apiKey = APIKey;
+        const apiSecret = APISecret;
+        const date = new Date().toGMTString();
+        const algorithm = 'hmac-sha256';
+        const headers = 'host date request-line';
+        const signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/iat HTTP/1.1`;
+        const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
+        const signature = CryptoJS.enc.Base64.stringify(signatureSha);
+        const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
+        const authorization = btoa(authorizationOrigin);
+        url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
+        return url;
+    }
+
+    function iatCountdown() {
+        let seconds = 60;
+        console.log(`录音中(${seconds}s)`);
+        iatCountdownInterval = setInterval(() => {
+            seconds -= 1;
+            if (seconds <= 0) {
+                clearInterval(iatCountdownInterval);
+                iatRecorder.stop();
+            } else {
+                console.log(`录音中(${seconds}s)`);
+            }
+        }, 1000);
+    }
+
+    function iatChangeBtnStatus(status) {
+        iatBtnStatus = status;
+        if (status === 'CONNECTING') {
+            console.log('建立连接中');
+            iatResultText = '';
+            iatResultTextTemp = '';
+        } else if (status === 'OPEN') {
+            if (iatRecorder) {
+                iatCountdown();
+            }
+        } else if (status === 'CLOSING') {
+            console.log('关闭连接中');
+        } else if (status === 'CLOSED') {
+            console.log('开始录音');
+        }
+    }
+
+    function toBase64(buffer) {
+        let binary = '';
+        const bytes = new Uint8Array(buffer);
+        const len = bytes.byteLength;
+        for (let i = 0; i < len; i += 1) {
+            binary += String.fromCharCode(bytes[i]);
+        }
+        return window.btoa(binary);
+    }
+
+    /* 语音识别 ↑*/
+
+    /*语音合成 ↓*/
+    // 合成对象
+    let audioPlayer = null
+    // 按钮状态:"UNDEFINED" "CONNECTING" "PLAY" "STOP"
+    let btnStatus = "UNDEFINED";
+    //
+    let ttsWS = null;
+
+    function initSpeechSynthesis(){
+        // 初始化语音播放
+        audioPlayer = new window.AudioPlayer("/ai/tts/dist");
+        // 开始播放
+        audioPlayer.onPlay = () => {
+            changeBtnStatus("PLAY");
+        };
+        audioPlayer.onStop = (audioDatas) => {
+            console.log('停止播放');
+            // 修改为undefined,下次播放重头开始
+            btnStatus === "PLAY" && changeBtnStatus("UNDEFINED");
+        };
+    }
+
+    function changeBtnStatus(status) {
+        btnStatus = status;
+        if (status === "UNDEFINED") {
+            console.log("立即合成");
+        } else if (status === "CONNECTING") {
+            console.log("正在合成");
+        } else if (status === "PLAY") {
+            console.log("停止播放");
+        } else if (status === "STOP") {
+            console.log("重新播放");
+        }
+    }
+
+    function getWebSocketUrl() {
+        const { APIKey, APISecret } = xfIatKeys;
+        if (!APIKey) {
+            message.error('语音合成配置未生效');
+            return null;
+        }
+        let url = "wss://tts-api.xfyun.cn/v2/tts";
+        let host = "tts-api.xfyun.cn";
+        const apiKey = APIKey;
+        const apiSecret = APISecret;
+        let date = new Date().toGMTString();
+        let algorithm = "hmac-sha256";
+        let headers = "host date request-line";
+        let signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/tts HTTP/1.1`;
+        let signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret);
+        let signature = CryptoJS.enc.Base64.stringify(signatureSha);
+        let authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`;
+        let authorization = btoa(authorizationOrigin);
+        url = `${url}?authorization=${authorization}&date=${date}&host=${host}`;
+        return url;
+    }
+
+    function encodeText(text, type) {
+        if (type === "unicode") {
+            let buf = new ArrayBuffer(text.length * 4);
+            let bufView = new Uint16Array(buf);
+            for (let i = 0, strlen = text.length; i < strlen; i++) {
+                bufView[i] = text.charCodeAt(i);
+            }
+            let binary = "";
+            let bytes = new Uint8Array(buf);
+            let len = bytes.byteLength;
+            for (let i = 0; i < len; i++) {
+                binary += String.fromCharCode(bytes[i]);
+            }
+            return window.btoa(binary);
+        } else {
+            return Base64.encode(text);
+        }
+    }
+
+    function ttsStartPlay(text, index) {
+        playActiveIndex.value = index
+        playButtonFlag.value = !playButtonFlag.value;
+        if (btnStatus === "UNDEFINED") {
+            // 开始合成
+            connectWebSocket(text.replace(/<\/?.+?\/?>/g,''));
+        } else if (btnStatus === "CONNECTING") {
+            // 停止合成
+            changeBtnStatus("UNDEFINED");
+            ttsWS?.close();
+            audioPlayer.reset();
+            return;
+        } else if (btnStatus === "PLAY") {
+            audioPlayer.stop();
+        } else if (btnStatus === "STOP") {
+            audioPlayer.play();
+        }
+    };
+
+    function connectWebSocket(text) {
+        const url = getWebSocketUrl();
+        if ("WebSocket" in window) {
+            ttsWS = new WebSocket(url);
+        } else if ("MozWebSocket" in window) {
+            ttsWS = new MozWebSocket(url);
+        } else {
+            alert("浏览器不支持WebSocket");
+            return;
+        }
+        changeBtnStatus("CONNECTING");
+
+        ttsWS.onopen = (e) => {
+            audioPlayer.start({
+                autoPlay: true,
+                sampleRate: 16000,
+                resumePlayDuration: 1000
+            });
+            changeBtnStatus("PLAY");
+            let tte = "UTF8";
+            let params = {
+                common: {
+                    app_id: xfIatKeys.APPID,
+                },
+                business: {
+                    aue: "raw",
+                    auf: "audio/L16;rate=16000",
+                    vcn: "xiaoyan",
+                    // vcn: "aisbabyxu",
+                    // vcn: "aisjinger",
+                    // vcn: "aisxping",
+                    // vcn: "aisjinger",
+                    // vcn: "x4_lingxiaoyao_em",
+                    // vcn: "x4_lingxiaoyao_en",
+                    speed: +50,
+                    volume: +50,
+                    pitch: +50,
+                    bgs: 1,
+                    tte,
+                },
+                data: {
+                    status: 2,
+                    text: encodeText(text, tte),
+                },
+            };
+            ttsWS.send(JSON.stringify(params));
+        };
+        ttsWS.onmessage = (e) => {
+            let jsonData = JSON.parse(e.data);
+            // 合成失败
+            if (jsonData.code !== 0) {
+                console.error(jsonData);
+                changeBtnStatus("UNDEFINED");
+                return;
+            }
+            audioPlayer.postMessage({
+                type: "base64",
+                data: jsonData.data.audio,
+                isLastData: jsonData.data.status === 2,
+            });
+            if (jsonData.code === 0 && jsonData.data.status === 2) {
+                ttsWS.close();
+            }
+        };
+        ttsWS.onerror = (e) => {
+            console.error(e);
+        };
+        ttsWS.onclose = (e) => {
+            // console.log(e);
+        };
+    }
+
+    /*语音合成 ↑*/
+
+    let chatContainerRef = ref(null)
+    function scrollToBottom(){
+        if(chatContainerRef.value.scrollHeight){
+            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight+20;
+        }
+    }
+
+    onMounted(()=>{
+        nextTick(()=>{
+            initSpeechRecognition();
+            initSpeechSynthesis();
+            list();
+        })
+
+    })
 
 </script>
 
-<style scoped>
+<style  scoped lang="scss">
+    p{
+        margin: 0;
+        padding: 0;
+    }
+    .ai_career_interview_chat{
+        background: url('@/assets/images/environment/ai-career-background.png') no-repeat;
+        background-size: 1920px 1080px;
+        z-index:10;
+        width: 100%;
+        height: 1080px;
+        display: flex;
+
+        .left-box{
+            /*position: absolute;*/
+            margin-top: 18px;
+            margin-left: 44px;
+            width: 428px;
+            height: 1044px;
+            background: rgba(255,255,255,0.4);
+            border-radius: 10px 10px 10px 10px;
+            .profile {
+                img{
+                    width: 117px;
+                    height: 136px;
+                    margin-top: 48px;
+                    margin-left: 145px;
+                }
+            }
+            .introduce-text{
+                width: 392px;
+                margin-left: 14px;
+                font-weight: 400;
+                font-size: 28px;
+                color: #7D7C7C;
+                line-height: 40px;
+                text-align: left;
+                font-style: normal;
+                text-transform: none;
+            }
+            .question-list{
+                width: 392px;
+                height: 500px;
+                overflow: auto;
+                margin-left: 14px;
+                font-weight: 400;
+                font-size: 28px;
+                color: #7D7C7C;
+                line-height: 40px;
+                text-align: left;
+                font-style: normal;
+                text-transform: none;
+                .question-list-item{
+                    cursor: pointer;
+                }
+            }
+        }
+
+        .center-box{
+            width: 1390px;
+            height: 1044px;
+            margin-top: 18px;
+            margin-left: 14px;
+            background: rgba(255,255,255,0.2);
+            border-radius: 10px 10px 10px 10px;
+            .center-header{
+                width: 1390px;
+                height: 95px;
+                background: #D8F9F2;
+                border-radius: 10px 10px 0px 0px;
+                font-weight: bold;
+                font-size: 30px;
+                color: #333333;
+                line-height: 52px;
+                letter-spacing: 3px;
+                text-align: left;
+                font-style: normal;
+                text-transform: none;
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+                .header-exit-btn{
+                    width: 136px;
+                    height: 74px;
+                    background: #FB5451;
+                    border-radius: 10px 10px 10px 10px;
+                    font-weight: 400;
+                    font-size: 24px;
+                    color: #FFFFFF;
+                    /*text-align: center;*/
+                    font-style: normal;
+                    text-transform: none;
+                    display: flex;
+                    align-items: center;
+                    justify-content: center;
+                    margin-right: 30px;
+                }
+            }
+            .chat-container {
+                display: flex;
+                flex-direction: column;
+                height: 865px;
+                /*聊天列表*/
+                .message-list{
+                    flex: 1;
+                    height: 865px;
+                    overflow-y: auto;
+                    display: flex;
+                    flex-direction: column;
+                    .user-message {
+                        display: flex;
+                        align-self: flex-end;
+                        flex-direction: row-reverse;
+                        align-items: center;
+                        margin: 4px;
+                        .user-message-content {
+                            padding: 10px;
+                            background: #EDFFFB;
+                            border-radius: 5px 5px 32px 5px;
+                        }
+                    }
+
+
+                    .bot-message {
+                        max-width: 70%;
+                        display: flex;
+                        align-self: flex-start;
+                        align-items: flex-start;
+                        margin: 4px;
+                        .bot-message-content {
+                            padding: 10px;
+                            background: #EDFFFB;
+                            border-radius: 5px 5px 5px 32px;
+
+                        }
+                    }
+                }
+            }
+            .input-container{
+
+            }
+        }
+    }
+
+    .content-text{
+        font-weight: 400;
+        font-size: 28px !important;
+        color: #333333;
+        line-height: 52px;
+        letter-spacing: 3px;
+        text-align: left;
+        font-style: normal;
+        text-transform: none;
+    }
+
+    .loading-dots {
+        padding-left: 5px;
+    }
+    .dot {
+        display: inline-block;
+        margin-left: 5px;
+        width: 8px;
+        height: 8px;
+        background-color: #000000;
+        border-radius: 50%;
+        animation: pulse 1.2s infinite ease-in-out both;
+    }
+    .dot:nth-child(2) {
+        animation-delay: -0.6s;
+    }
+
+    @keyframes pulse {
+        0%,
+        100% {
+            transform: scale(0.6);
+            opacity: 0.4;
+        }
 
+        50% {
+            transform: scale(1);
+            opacity: 1;
+        }
+    }
 </style>