Browse Source

[feat][ai聊天][语音识别]

hizhangling 5 days ago
parent
commit
75ea3be56a

File diff suppressed because it is too large
+ 1 - 0
public/ai/tts/dist/index.cjs.js


+ 35 - 0
public/ai/tts/dist/index.d.ts

@@ -0,0 +1,35 @@
+type ISaveAudioData = "pcm" | "wav";
+declare class AudioPlayer {
+    constructor(processorPath?: string);
+    private toSampleRate;
+    private resumePlayDuration;
+    private fromSampleRate;
+    private isAudioDataEnded;
+    private playAudioTime?;
+    private status;
+    private audioContext?;
+    private bufferSource?;
+    private audioDatas;
+    private pcmAudioDatas;
+    private audioDataOffset;
+    private processor;
+    postMessage({ type, data, isLastData }: {
+        type: "base64" | "string" | "Int16Array" | "Float32Array";
+        data: string | Int16Array | Float32Array;
+        isLastData: boolean;
+    }): void;
+    private onPlay?;
+    private onStop?;
+    private playAudio;
+    reset(): void;
+    start({ autoPlay, sampleRate, resumePlayDuration }?: {
+        autoPlay?: boolean;
+        sampleRate?: number;
+        resumePlayDuration?: number;
+    }): void;
+    play(): void;
+    stop(): void;
+    getAudioDataBlob(type: ISaveAudioData): Blob | undefined;
+}
+
+export { AudioPlayer as default };

File diff suppressed because it is too large
+ 1 - 0
public/ai/tts/dist/index.esm.js


File diff suppressed because it is too large
+ 1 - 0
public/ai/tts/dist/index.umd.js


File diff suppressed because it is too large
+ 1 - 0
public/ai/tts/dist/processor.worker.js


+ 61 - 0
src/utils/scroll.js

@@ -0,0 +1,61 @@
+/**
+ * 设置滚动位置
+ * @param {Object} recordWrapperTarget - 滚动容器ref对象
+ * @param {string} behavior - 滚动行为,默认为"smooth"
+ * @param {boolean} autoScroll - 是否自动滚动到底部,默认为true
+ */
+export const setScroll = (recordWrapperTarget, behavior = "smooth", autoScroll = true) => {
+    if (!recordWrapperTarget || !recordWrapperTarget.value) return;
+
+    const container = recordWrapperTarget.value;
+    const scrollOptions = { behavior };
+
+    // 初始化滚动状态
+    if (!container._scrollState) {
+        container._scrollState = {
+            autoScrollEnabled: true,
+            lastScrollTime: 0,
+            timeout: null
+        };
+
+        container.addEventListener('scroll', () => {
+            const { scrollTop, scrollHeight, clientHeight } = container;
+            const now = Date.now();
+            const distanceToBottom = scrollHeight - (scrollTop + clientHeight);
+
+            // 检测向上滚动(scrollTop减小)
+            if (scrollTop < container._scrollState.lastScrollTop) {
+                container._scrollState.autoScrollEnabled = false;
+            }
+
+            // 滚动到底部时恢复自动滚动
+            if (distanceToBottom < 10) {
+                container._scrollState.autoScrollEnabled = true;
+            }
+
+            // 记录当前滚动位置
+            container._scrollState.lastScrollTop = scrollTop;
+
+            // 自动滚动保护期
+            if (container._scrollState.isAutoScrolling) {
+                setTimeout(() => {
+                    container._scrollState.isAutoScrolling = false;
+                }, 100);
+            }
+        });
+    }
+
+    // 自动滚动逻辑
+    if (autoScroll && container._scrollState.autoScrollEnabled) {
+        scrollOptions.top = container.scrollHeight;
+        container._scrollState.lastScrollTime = Date.now();
+
+        // 清除之前的超时
+        clearTimeout(container._scrollState.timeout);
+
+        // 设置新的滚动
+        container._scrollState.timeout = setTimeout(() => {
+            container.scrollTo(scrollOptions);
+        }, 50);
+    }
+};

+ 61 - 22
src/views/xjc-integratedmachine/common/ai/chat.vue

@@ -13,36 +13,36 @@
             </div>
         </div>
         <div class="main-content">
-            <div class="chat-container">
+            <div class="chat-container" ref="chatContainerRef">
                 <div class="message-list">
                     <div v-for="(item, index) in chatRecordList" :key="item.id">
                         <!-- 会话图标 -->
                         <!--<div :class="item.isUser == 1? 'user-image' : 'system-image' "></div>-->
                         <div v-if="item.isUser == 1" class="user-message">
-                            <img src="@/assets/images/common/ai/user.png" alt="icon"/>
+                            <img src="@/assets/images/common/ai/user.png" alt="icon" style="margin-left: 4px"/>
                             <span class="user-message-content">{{item.content}}</span>
                         </div>
-                        <div v-else class="message bot-message">
-                            <img src="@/assets/images/common/ai/system.png" alt="icon"/>
-                            <div v-show="!expandIndexList.includes(index)" class="bot-message-content" v-html="item.content.substring(0, 80)"></div>
-                            <div v-show="expandIndexList.includes(index)" class="bot-message-content" v-html="item.content"></div>
-                            <el-button v-if="item.content.length>80" @click="foldOrExpandMessage(index, expandIndexList.includes(index))">{{expandIndexList.includes(index)?'折叠':'展开'}}</el-button>
+                        <div v-else class="bot-message">
+                            <img src="@/assets/images/common/ai/system.png" alt="icon" style="margin-left: 4px"/>
+                            <div v-show="!expandIndexList.includes(index)" class="bot-message-content" style="margin-left: 4px" v-html="item.content.substring(0, 80)"></div>
+                            <div v-show="expandIndexList.includes(index)" class="bot-message-content" style="margin-left: 4px" v-html="item.content"></div>
+                            <el-button v-if="item.content.length>80" style="margin-left: 4px" @click="foldOrExpandMessage(index, expandIndexList.includes(index))">{{expandIndexList.includes(index)?'折叠':'展开'}}</el-button>
                             <el-button>播放</el-button>
                         </div>
                     </div>
-                </div>
-                <div class="message-box">
-                    <div class="typing-message">
-                        <span>
-                            <span v-html="streamHtmlData"></span>
-                            <span class="loading-dots">
-                                <span class="dot"></span>
-                                <span class="dot"></span>
+                    <div class="bot-message" v-if="streamHtmlData">
+                        <img src="@/assets/images/common/ai/system.png" alt="icon"/>
+                        <div class="bot-message-content" v-html="streamHtmlData"></div>
+                        <div class="typing">
+                            <span>
+                                <span class="loading-dots">
+                                    <span class="dot"></span>
+                                    <span class="dot"></span>
+                                </span>
                             </span>
-                        </span>
+                        </div>
                     </div>
                 </div>
-                <el-button @click="stopMessage">停止回答</el-button>
             </div>
             <div class="input-container">
                 <el-input
@@ -65,6 +65,9 @@
                 <div class="control-button">
                     <el-button style="width:140px" @click="closeVoiceOpen">结束</el-button>
                 </div>
+                <div class="control-button">
+                    <el-button style="width:140px" @click="stopMessage">停止回答</el-button>
+                </div>
             </div>
             <div class="session-list-box">
 
@@ -74,14 +77,17 @@
 </template>
 
 <script setup>
-    import {nextTick, onMounted} from 'vue'
-    import {aiChatRecordList} from '@/api/xjc-integratedmachine/common/aiChat.js'
+    import {nextTick, onMounted, useTemplateRef} from 'vue'
+    import {aiChatRecordList, aiChatRecordAdd} from '@/api/xjc-integratedmachine/common/aiChat.js'
     import { getToken } from '@/utils/auth'
+    import { setScroll } from '@/utils/scroll.js'
     // md转换为html
     import { marked } from 'marked'
     // 语音识别
     import CryptoJS from 'crypto-js';
     import * as RecorderManager from "/public/ai/iat/dist/index.umd.js"
+    import * as AudioPlayer from "/public/ai/tts/dist/index.umd.js"
+
 
     // 聊天记录
     let chatRecordList = ref([])
@@ -116,6 +122,16 @@
         })
     }
 
+    function addRecord(content) {
+        let queryForm = {
+            content: content,
+            isUser: 0
+        }
+        aiChatRecordAdd(queryForm).then(resp =>{
+            console.log(resp)
+
+        })
+    }
     const sendMessage = () => {
         if (inputMessage.value.trim()) {
             sendRequest(inputMessage.value.trim())
@@ -131,7 +147,7 @@
             isTyping: false
         }
         // 消息加入聊天记录
-        // chatRecordList.value.push(userMessage)
+        chatRecordList.value.push(userMessage)
         try{
             isLoading.value = true;
             streamMarkdownData.value = ''; // 清空之前的数据
@@ -177,6 +193,7 @@
         } finally {
             streamHtmlData.value = marked(streamMarkdownData.value)
             isLoading.value = false;
+            addRecord(streamHtmlData.value);
         }
     }
 
@@ -229,7 +246,7 @@
         APIKey: '8b1a53486bec887eb817b4410aa743ed',
     }
     // 初始化语音识别
-    function initRecognize(){
+    function initSpeechRecognition(){
         // 初始化录音
         recorder = new window.RecorderManager('/ai/iat/dist');
         // 开始录音
@@ -464,10 +481,31 @@
     }
     /* 语音识别 ↑*/
 
+    /*语音合成 ↓*/
+
+
+    function initSpeechsynthesis(){
+
+    }
+
+    /*语音合成 ↑*/
+
+    let chatContainerRef = ref(null)
+    let chatContainerRefObj = useTemplateRef(chatContainerRef)
+
+
+    const scrollToBottom = () => {
+        nextTick(() => {
+            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight;
+        });
+    };
 
     onMounted(()=>{
         nextTick(()=>{
-            initRecognize();
+            // 组件挂载后自动滚动到底部
+            scrollToBottom();
+            initSpeechRecognition();
+            initSpeechsynthesis();
             sendMessage()
         })
 
@@ -647,6 +685,7 @@
     /*输入框*/
     .input-container {
         display: flex;
+        align-items: center;
     }
 
     .input-container .el-input {