基于 Vue 的Deepseek流式加載對話Demo

目錄

    • 引言
    • 組件概述
    • 核心組件與功能實現
      • 1. 消息顯示組件(Message.vue)
      • 2. 輸入組件(Input.vue)
      • 3. 流式請求處理(useDeepseek.ts)
      • 4. 語音處理模塊(Voice.vue)
    • 總結
    • Demo Github 地址

引言

在當今數字化時代,智能對話系統的應用越來越廣泛,如客服聊天機器人、智能助手等。本文將詳細介紹一個基于 Vue 框架開發的智能對話系統的實現過程,該系統支持文本輸入、語音輸入、流式響應等功能,讓我們一步步揭開它的神秘面紗。

在這里插入圖片描述

組件概述

整個組件主要由以下幾個核心部分組成:

  1. 用戶界面組件:負責與用戶進行交互,包括文本輸入框、語音輸入按鈕、消息顯示區域等。
  2. 消息處理組件:將用戶輸入的消息進行處理,并顯示在界面上。
  3. 流式請求處理:與后端進行流式通信,實時獲取響應內容。
  4. 語音處理模塊:支持語音輸入功能,將語音轉換為文本。

核心組件與功能實現

1. 消息顯示組件(Message.vue)

該組件用于顯示用戶和系統的消息。在這個組件中,我們使用了 Markdown 解析器來處理消息內容,支持代碼高亮和自定義標簽。

<template><divclass="msg-item":class="{'msg-item-system': role === 'system'}"><divclass="msg-content":class="{'msg-content-user': role === 'user'}"><span class="msg-pop-container"><spanclass="msg-pop-default"v-html="mkHtml"ref="popRef":class="{'msg-pop-primary': role === 'user'}"></span></span></div></div>
</template><script lang="ts" setup>
import MarkdownIt from "markdown-it";
import mk from "markdown-it-katex";
import hljs from "highlight.js";
import "highlight.js/styles/atom-one-dark-reasonable.css";
import { computed, nextTick, ref } from "vue";interface Props {role: string;content: string;streaming?: boolean;
}const props = withDefaults(defineProps<Props>(), {content: "",streaming: false
});const md: MarkdownIt = MarkdownIt({highlight: function (str: string, lang: string) {if (lang && hljs.getLanguage(lang)) {try {return `<div class="hl-code"><div class="hl-code-header"><span>${lang}</span></div><div class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></div></div>`;} catch (__) {console.log(__, "error");}}return `<div class="hl-code"><div class="hl-code-header"><span>${lang}</span></div><div class="hljs"><code>${md.utils.escapeHtml(str)}</code></div></div>`;}
});md.use(mk, {throwOnError: false,errorColor: " #cc0000"
});// 自定義規則函數,用于解析 <think> 標簽
function thinkTagRule(state, startLine, endLine, silent) {let pos,max,nextLine,token,autoClosed = false;let start = state.bMarks[startLine] + state.tShift[startLine];let end = state.eMarks[startLine];// 檢查是否以 <think> 開頭if (start + 7 > end || state.src.slice(start, start + 7) !== "<think>") {return false;}// 跳過 <think>pos = start + 7;// 查找 </think> 結束標簽for (nextLine = startLine; ; nextLine++) {if (nextLine >= endLine) {// 未找到結束標簽break;}max = state.bMarks[nextLine] + state.tShift[nextLine];if (max < state.eMarks[nextLine] && state.src.slice(max, max + 8) === "</think>") {// 找到結束標簽autoClosed = true;break;}}// 如果處于靜默模式,只驗證標簽,不生成 tokenif (silent) {return autoClosed;}// 創建一個新的 token 表示開始標簽token = state.push("think_tag_open", "think", 1);token.markup = "<think>";token.map = [startLine, nextLine];// 處理標簽內部的內容state.md.block.tokenize(state, startLine + 1, nextLine);// 創建結束 tokentoken = state.push("think_tag_close", "think", -1);token.markup = "</think>";// 更新狀態,跳過已處理的行state.line = nextLine + 1;return true;
}// 將自定義規則添加到 MarkdownIt 實例中
md.block.ruler.before("paragraph", "think_tag", thinkTagRule);// 自定義渲染規則,將 <think> 標簽渲染為 <span class="think">
md.renderer.rules.think_tag_open = function () {return '<span class="think">';
};md.renderer.rules.think_tag_close = function () {return "</span>";
};function findLastElement(element: HTMLElement): HTMLElement {if (!element.children.length) {return element;}const lastChild = element.children[element.children.length - 1];if (lastChild.nodeType === Node.ELEMENT_NODE) {return findLastElement(lastChild as HTMLElement);}return element;
}const popRef = ref();
const mkHtml = computed(() => {if (props.role === "user") {return props.content;}let html = md.render(props.content);console.log(html); // 調試信息nextTick(() => {if (props.streaming) {const parent = popRef.value;if (!parent) return;let lastChild = parent.lastElementChild || parent;console.log(lastChild.tagName);if (lastChild.tagName === "PRE") {lastChild = lastChild.getElementsByClassName("hljs")[0] || lastChild;}if (lastChild.tagName === "OL") {lastChild = findLastElement(lastChild as HTMLElement);}lastChild?.insertAdjacentHTML("beforeend", '<span class="input-cursor"></span>');}});return html;
});
</script><style lang="scss" scoped>
.msg-item {width: 100%;display: flex;margin-bottom: 10px;padding: 0 10px;border-radius: 4px;.msg-content {position: relative;width: 100%;flex: 1 1 auto;.msg-pop-container {position: relative;display: inline-block;max-width: 95%;.msg-pop-default {width: 100%;display: inline-block;padding: 8px;background: #f5f5f5;border-radius: 4px;color: #252724;:deep(p) {margin-bottom: 0;white-space: pre-line;}}.msg-pop-primary {background: #95ec69;// white-space: pre-line;}}}
}.msg-content-user {text-align: end;
}
.msg-item-system {justify-content: flex-end;
}
</style><style lang="scss">
.think {color: blue;font-style: italic;
}
.hl-code {margin-top: 1em;
}.hl-code-header {padding: 0 10px;color: #abb2bf;background: #1d2635;border-radius: 4px 4px 0 0;display: flex;justify-content: space-between;align-items: center;
}.hljs {padding: 10px;overflow-x: auto;border-radius: 0 0 4px 4px;.input-cursor {background: #fff;/* fallback for old browsers */}
}.input-cursor {position: relative;display: inline-flex;align-items: center;width: 1px;height: 1em;background: #3b414b;/* fallback for old browsers */padding-left: 0.05em;top: 0.1em;animation: blink 1s steps(1) infinite;
}@keyframes blink {0% {visibility: visible;}50% {visibility: hidden;}100% {visibility: visible;}
}
</style>

2. 輸入組件(Input.vue)

該組件提供了文本輸入框和語音輸入按鈕,支持用戶輸入問題并發送,同時可以開啟新的對話。

<template><div class="msg-editor-container"><!-- 文本輸入框 --><div class="flex items-center"><el-inputclass="flex-1"ref="inputDiv"v-model="inputValue"type="textarea":autosize="{ minRows: 2, maxRows: 4 }"placeholder="請輸入你的問題"@keydown.enter.exact="handleKeydown"></el-input><!-- 語音 --><DeepseekVoiceVue @voiceTextChange="voiceTextChange"></DeepseekVoiceVue><!-- 新對話 --><el-tooltip :z-index="100000" effect="dark" content="新對話" placement="top"><el-icon class="mr-1 focus:border-blue-400 hover:bg-[#f5f5f5] bg-white cursor-pointer" size="22px" @click="newSessionBtn"><Plus /></el-icon></el-tooltip><!-- 操作按鈕 --><el-tooltip :z-index="100000" effect="dark" content="發送" placement="top"><el-button type="primary" icon="Top" circle @click="handleKeydown"></el-button></el-tooltip></div></div>
</template><script lang="ts" setup>
import { ref } from "vue";
import DeepseekVoiceVue from "./Voice.vue";const emits = defineEmits(["submit", "newSession"]);
const inputValue = ref("");const voiceTextChange = (text: string) => {console.log(text);inputValue.value = inputValue.value + text;
};const handleKeydown = (e: Event) => {e.stopPropagation();e.returnValue = false;if (inputValue.value === "") return;emits("submit", inputValue.value);inputValue.value = "";
};const newSessionBtn = () => {emits("newSession");
};
</script><style lang="scss" scoped>
.msg-editor-container {border: 1px solid #dee0e3;border-radius: 4px;padding: 5px;
}.operationBtn {display: flex;
}
</style>

3. 流式請求處理(useDeepseek.ts)

通過useGpt鉤子函數處理 GPT 流式請求,使用Typewriter類模擬打字效果,讓響應內容逐字顯示。

import { ref } from "vue";
import { StreamGpt, Typewriter, GptMsgs, RequestData } from "./fetchApi";// useGpt 鉤子函數,用于處理 GPT 流式請求
export const useGpt = (key: string) => {const streamingText = ref("");const streaming = ref(false);const msgList = ref<GptMsgs>([]);const sessionId = ref("");// 初始化 Typewriter 實例const typewriter = new Typewriter((str: string) => {streamingText.value += str || "";// console.log("str", str);});// 初始化 StreamGpt 實例const gpt = new StreamGpt(key, {onStart: (prompt: string) => {streaming.value = true;msgList.value.push({role: "user",content: prompt});},onPatch: (text: string) => {// console.log("onPatch", text);typewriter.add(text);},onCreated: () => {typewriter.start();},onDone: () => {typewriter.done();streaming.value = false;msgList.value.push({role: "system",content: streamingText.value});streamingText.value = "";}});// 發送流式請求const stream = (requestData: RequestData) => {if (sessionId.value === "") {sessionId.value = generateUUID();}gpt.stream({ ...requestData, sessionId: sessionId.value });};// 新會話const newSession = () => {msgList.value = [];streamingText.value = "";sessionId.value = generateUUID();};// 生成 UUIDconst generateUUID = () => {let uuid = "";for (let i = 0; i < 32; i++) {const random = (Math.random() * 16) | 0;if (i === 8 || i === 12 || i === 16 || i === 20) uuid += "-";uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16);}return uuid;};return {streamingText,streaming,msgList,stream,newSession};
};

4. 語音處理模塊(Voice.vue)

該模塊支持語音錄制和識別,將錄制的語音轉換為 WAV 文件或Base64,并發送到后端進行識別。

<template><!-- 語音輸入按鈕 --><button @click="toggleRecording">{{ isRecording ? '停止錄音' : '開始錄音' }}</button>
</template><script lang="ts" setup>
import { ref } from "vue";const isRecording = ref(false);
const mediaRecorder: any = ref(null);
const chunks: any = ref([]);const emits = defineEmits(["voiceTextChange"]);// 開始錄音
const startRecording = async () => {try {const stream = await navigator.mediaDevices.getUserMedia({ audio: true });mediaRecorder.value = new MediaRecorder(stream);mediaRecorder.value.ondataavailable = (event: any) => {if (event.data.size > 0) {chunks.value.push(event.data);}};mediaRecorder.value.onstop = () => {const wavBlob = encodeWAV(chunks.value);wavTransformBase64(wavBlob);chunks.value = [];};mediaRecorder.value.start();isRecording.value = true;} catch (error) {console.error("錄音失敗:", error);}
};// 停止錄音
const stopRecording = () => {if (mediaRecorder.value) {mediaRecorder.value.stop();isRecording.value = false;}
};// 生成WAV文件
function encodeWAV(chunks) {const totalLength = chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0);const header = generateWavHeader(totalLength);const wavBuffer = new Uint8Array(header.byteLength + totalLength);// 合并頭和數據wavBuffer.set(new Uint8Array(header.buffer), 0);let offset = header.byteLength;chunks.forEach(chunk => {wavBuffer.set(new Uint8Array(chunk.buffer), offset);offset += chunk.byteLength;});return new Blob([wavBuffer], { type: "audio/wav" });
}// 生成WAV文件頭
function generateWavHeader(dataLength) {const header = new ArrayBuffer(44);const view = new DataView(header);// RIFF標識writeString(view, 0, "RIFF");// 文件長度(數據長度 + 36)view.setUint32(4, dataLength + 36, true);// WAVE格式writeString(view, 8, "WAVE");// fmt子塊writeString(view, 12, "fmt ");// fmt塊長度(16字節)view.setUint32(16, 16, true);// 格式類型(1=PCM)view.setUint16(20, 1, true);// 聲道數view.setUint16(22, WAV_CONFIG.channelCount, true);// 采樣率view.setUint32(24, WAV_CONFIG.sampleRate, true);// 字節率view.setUint32(28, WAV_CONFIG.sampleRate * WAV_CONFIG.channelCount * (WAV_CONFIG.bitDepth / 8), true);// 塊對齊view.setUint16(32, WAV_CONFIG.channelCount * (WAV_CONFIG.bitDepth / 8), true);// 位深度view.setUint16(34, WAV_CONFIG.bitDepth, true);// data標識writeString(view, 36, "data");// 數據長度view.setUint32(40, dataLength, true);return view;
}// 寫入字符串到DataView
function writeString(view, offset, string) {for (let i = 0; i < string.length; i++) {view.setUint8(offset + i, string.charCodeAt(i));}
}// 將WAV文件轉換為Base64編碼
function wavTransformBase64(wavBlob) {const reader = new FileReader();reader.onload = function (e) {console.log("wav base64:", e.target.result);getRecognition(e.target.result);};reader.readAsDataURL(wavBlob);
}// 切換錄音狀態
const toggleRecording = () => {if (isRecording.value) {stopRecording();} else {startRecording();}
};const getRecognition = base64Str => {isLoading.value = true;Recognition(base64Str).then(res => {emits("voiceTextChange", res.data);}).finally(() => {console.log("voiceTextChange");isLoading.value = false;});
};
</script>

總結

通過以上步驟,我們實現了一個基于 Vue 的智能對話系統,支持文本輸入、語音輸入和流式響應。在開發過程中,我們使用了 Vue 的響應式原理和組件化開發思想,結合 Markdown 解析器、語音處理 API 和流式請求技術,為用戶提供了一個流暢、智能的對話體驗。未來,我們可以進一步優化系統性能,增加更多的功能,如多語言支持、情感分析等。

Demo Github 地址

stream-deepseek

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/72091.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/72091.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/72091.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RK3588 編譯 openssl

在編譯 OpenSSL 時,你需要確保你的系統環境已經配置好了所有必要的依賴和編譯工具。下面是一般步驟和一些常見問題的解決方案,特別是在使用 RK3588 這類的 ARM 處理器上。 1. 安裝依賴 首先,你需要安裝編譯 OpenSSL 所需的依賴。這通常包括編譯器(如 GCC)、make 工具、Per…

常見JVM命令

1. java -XX:PrintCommandLineFlags HelloGC 作用&#xff1a;打印 JVM 啟動時的命令行參數&#xff0c;包括用戶顯式設置的參數和 JVM 自動默認設置的參數。用于確認 JVM 實際使用的配置。 2. java -Xmn10M -Xms40M -Xmx60M -XX:PrintCommandLineFlags -XX:PrintGC -XX:Prin…

easy-poi導出and導入一對多數據excel

easy-poi導出and導入一對多數據excel 一、導入jar包 <!-- easy-poi --><dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-spring-boot-starter</artifactId><version>4.4.0</version></dependency> 二…

c#如何直接獲取json中的某個值

在 C# 中直接獲取 JSON 中的某個值,通常可以通過以下方法實現(以 Newtonsoft.Json 和 .NET 內置的 System.Text.Json 為例): 方法 1:使用 System.Text.Json(.NET 內置庫) using System.Text.Json;// 示例 JSON 字符串 string json = @"{""name"&qu…

WPS二次開發系列:Android 第三方應用如何獲取WPS端內文檔

1.需求場景 在項目開發中碰到這種情況&#xff0c;我們需要利用WPS的文檔管理能力&#xff0c;比如需要調用WPS的文件選擇器&#xff0c;來選擇文檔&#xff0c;同時需要得到WPS選擇的文檔結果返回給我們的應用。之前在網上找到了很久都沒有找到WPS移動端有相關的API接口文檔和…

Pytesseract識別圖片

1. Pytesseract識別圖片原理 1.1 Tesseract引擎工作原理 Tesseract OCR 引擎是一個功能強大的開源文字識別工具&#xff0c;其工作原理可以分為以下幾個關鍵步驟&#xff1a; 圖像預處理&#xff1a;Tesseract 首先對輸入的圖像進行預處理&#xff0c;包括灰度化、二值化、去…

Flutter 基礎組件 Text 詳解

目錄 1. 引言 2. 基本使用 3. 自定義樣式 4. 文本對齊與溢出控制 5. 外邊距 5.1 使用 Container 包裹 5.2 使用 Padding 組件 5.3 在 Row/Column 中使用 5.4 動態邊距調整 5.5 關鍵區別說明 5.6 設置 margin 無效 6. 結論 相關推薦 1. 引言 Text 組件是 Flutter 中…

Acknowledgment.nack方法重試消費kafka消息異常

文章目錄 問題示例異常 原因nack方法Acknowledgment接口實現類&#xff1a;ConsumerAcknowledgment實現類&#xff1a;ConsumerBatchAcknowledgment 解決方案1 批量消費指定index示例 2 單條消費示例 問題 使用BatchAcknowledgingMessageListener 批量消費Kafka消息&#xff0…

Java 反序列化 - commons collection 之困(一)

#01多余的碎碎念 說到 java 反序列化&#xff0c;去搜索的話能看到網上有很多分析關于 commons collection 利用鏈的文章&#xff0c;emm 我一開始看不懂&#xff0c;看到很多代碼的圖頭暈。 這篇文章的話其實是我跟著 p 神的文章一路走下來的&#xff0c;所以整個邏輯會按照…

python LLM工具包

阿里云鏡像pypi http://mirrors.aliyun.com/pypi/simple/ modelscope魔塔 pip install modelscope https://modelscope.cn/docs/models/download Sentence-transformers pip install -U sentence-transformers pip3 install torch -i https://pypi.tuna.tsinghua.edu.cn/sim…

Linux賬號和權限管理

用戶賬戶管理 理論 /etc/passwd 該目錄用于保存用戶名&#xff0c;宿主目錄&#xff0c;登錄shel等基本信息 /etc/shadow 該目錄用于保存 用戶密碼&#xff0c;賬戶有效期等信息 圖上每一行中都有用“&#xff1a;”隔斷的字段 字段含義&#xff1a; 第1字段:用戶賬號的名…

晉升系列4:學習方法

每一個成功的人&#xff0c;都是從底層開始打怪&#xff0c;不斷的總結經驗&#xff0c;一步一步打上來的。在這個過程中需要堅持、總結方法論。 對一件事情長久堅持的人其實比較少&#xff0c;在堅持的人中&#xff0c;不斷的總結優化的更少&#xff0c;所以最終達到高級別的…

win32匯編環境,對話框中使用樹形視圖示例四

;運行效果,當點擊張遼時,展示張遼的圖像 ;當點擊曹仁時,展示曹仁的圖像 ;win32匯編環境,對話框中使用樹形視圖示例四 ;當點擊樹形視圖treeview控件中的某項時,展示某些功能。這里展示的是當點到某個將領時,顯示某個將領的圖像 ;直接抄進RadAsm可編譯運行。重要部分加備注。…

智慧停車小程序:實時車位查詢、導航與費用結算一體化

智慧停車小程序:實時車位查詢、導航與費用結算一體化 一、城市停車困境的數字化突圍 中國機動車保有量突破4.3億輛,但車位供給缺口達8000萬。傳統停車管理模式存在三大致命傷: 盲盒式尋位:62%的車主遭遇"地圖顯示有位,到場已滿員"的窘境迷宮式導航:商場停車場…

Windows server網絡安全

摘要 安全策略 IP安全策略&#xff0c;簡單的來說就是可以通過做相應的策略來達到放行、阻止相關的端口&#xff1b;放行、阻止相關的IP&#xff0c;如何做安全策略&#xff0c;小編為大家詳細的寫了相關的步驟&#xff1a; 解說步驟&#xff1a; 阻止所有&#xff1a; 打…

充電樁快速搭建springcloud(微服務)+前后端分離(vue),客戶端實現微信小程序+ios+app使用uniapp(一處編寫,處處編譯)

充電樁管理系統是專為中小型充電樁運營商、企業和個人開發者設計的一套高效、靈活的管理平臺。系統基于Spring Cloud微服務架構開發&#xff0c;采用模塊化設計&#xff0c;支持單機部署與集群部署&#xff0c;能夠根據業務需求動態擴展。系統前端使用uniapp框架&#xff0c;可…

小肥柴慢慢手寫數據結構(C篇)(4-3 關于棧和隊列的討論)

小肥柴慢慢學習數據結構筆記&#xff08;C篇&#xff09;&#xff08;4-3 關于棧和隊列的討論&#xff09; 目錄1 雙端棧/隊列2 棧與隊列的相互轉化2-1 棧轉化成隊列2-2 隊列轉化成棧 3 經典工程案例3-1 生產者和消費者模型&#xff08;再次重溫環形緩沖區&#xff09;3-2 MapR…

labview實現大小端交換移位

在解碼時遇到了大小端交換的問題&#xff0c;需要把高低字節的16進制值進行互換&#xff0c;這里一時間不知道怎么操作&#xff0c;本來打算先把16進制轉字節數組&#xff0c;算出字節數組的大小&#xff0c;然后通過模2得到0&#xff0c;1&#xff0c;來判斷是否為奇數位和偶數…

在Windows系統上安裝和配置Redis服務

&#x1f31f; 在Windows系統上安裝和配置Redis服務 Redis是一個高性能的鍵值存儲數據庫&#xff0c;廣泛用于緩存、消息隊列和實時分析等場景。雖然Redis最初是為Linux設計的&#xff0c;但也有Windows版本可供使用。今天&#xff0c;我將詳細介紹如何在Windows系統上安裝Red…

Ateme在云端構建可擴展視頻流播平臺

Akamai Connected Cloud幫助Ateme客戶向全球觀眾分發最高質量視頻內容。 “付費電視運營商和內容提供商現在可以在Akamai Connected Cloud上通過高質量視頻吸引觀眾&#xff0c;并輕松擴展。”── Ateme首席戰略官Rmi Beaudouin ? Ateme是全球領先的視頻壓縮和傳輸解決方案提…