vue2實現類似chatgpt和deepseek的AI對話流打字機效果,實現多模型同時對話

實現多模型同時對話

功能特點:

1、抽離對話框成單獨組件ChatBox.vue,在新增模型對比窗口時可重復利用

2、通過sse與后臺實時數據流,通過定時器實現打字效果

3、適應深度思考內容輸出,可點擊展開與閉合

4、可配置模型參數,本地存儲當前模型參數和對話記錄,頁面關閉時清除

5、通過是否響應<think>標簽來識別是否有深度思考內容

6、通過響應的finishReason字段,識別回答是否已停止(null正常回答,stop回答結束,length超出文本)

安裝插件

highlight.js、markdown-it

創建對話窗口組件ChatBox.vue

<template><el-card class="box-card"><div slot="header" class="clearfix"><div class="header-item-box"><vxe-select v-model="modelType"><vxe-optionv-for="(item, i) in modelTypeList":key="i":value="item.id":label="item.modelName"></vxe-option></vxe-select><div><vxe-button@click="handleDeleteCurModel"type="text"icon="iconfont icon-zhiyuanfanhui9"v-if="modelIndex !== 1"></vxe-button><vxe-button@click="handleParamsConfig"type="text"icon="vxe-icon-setting"></vxe-button></div></div></div><div ref="logContainer" class="talk-box"><div class="talk-content"><el-rowv-for="(item, i) in contentList":key="i"class="chat-assistant"><transition name="fade"><div:class="['answer-cont', item.type === 'user' ? 'end' : 'start']"><img v-if="item.type == 'assistant'" :src="welcome.icon" /><div :class="item.type === 'user' ? 'send-item' : 'answer-item'"><divv-if="item.type == 'assistant'"class="hashrate-markdown"v-html="item.message"/><div v-else>{{ item.message }}</div></div></div></transition></el-row></div></div><ModelParamConfig ref="ModelParamConfigRef"></ModelParamConfig></el-card>
</template><script>
import hljs from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
import MarkdownIt from 'markdown-it'
import { chatSubmit, chatCancel } from '@/api/evaluationServer/modelTalk.js'
import { mapGetters } from 'vuex'
import { sse } from '@/utils/sse.js'
import ModelParamConfig from '@/views/evaluationServer/modelTalk/components/ModelParamConfig.vue'
window.hiddenThink = function (index) {// 隱藏思考內容if (document.getElementById(`think_content_${index}`).style.display == 'none') {document.getElementById(`think_content_${index}`).style.display = 'block'document.getElementById(`think_icon_${index}`).classList.replace('vxe-icon-arrow-up', 'vxe-icon-arrow-down')} else {document.getElementById(`think_content_${index}`).style.display = 'none'document.getElementById(`think_icon_${index}`).classList.replace('vxe-icon-arrow-down', 'vxe-icon-arrow-up')}
}
export default {props: {modelTypeList: {type: Array,default: () => []},modelIndex: {type: Number,default: 1},/*** 模型窗口*/modelDomIndex: {type: Number,default: 0}},components: { ModelParamConfig },computed: {...mapGetters(['token'])},data() {return {modelType: '',inputMessage: '',contentList: [],answerTitle: '',thinkTime: null,startAnwer: false,startTime: null,endTime: null,typingInterval: null,msgHight: null,welcome: {title: '',desc: '',icon: require('@/assets/images/kubercloud-logo.png')},markdownIt: {},historyList: [], //記錄發送和回答的純文本 。user提問者,assistant回答者lastScrollHeight: 0}},mounted() {setTimeout(() => {this.markdownIt = MarkdownIt({html: true,linkify: true,highlight: function (str, lang) {if (lang && hljs.getLanguage(lang)) {try {return hljs.highlight(str, { language: lang }).value} catch (__) {console.log(__)}}return ''}})if (this.modelTypeList && this.modelTypeList.length) {this.modelType = this.modelTypeList[0].id}}, 500)},methods: {sleep(ms) {return new Promise(resolve => setTimeout(resolve, ms))},handleDeleteCurModel() {this.$emit('handleDeleteCurModel', this.modelIndex)},clearHistory() {this.contentList = []this.historyList = []},async sendMessage({ message }) {this.inputMessage = messageconst name = this.modelTypeList.find(item => item.id === this.modelType)?.nameif (!name) returnlet params = {historyList: [...this.historyList],text: this.inputMessage,deployId: this.modelType,temperature: 1,maxTokens: 1024,topP: 1,seed: '',stopSequence: '',modelName: name}let modelParams = sessionStorage.getItem('modelTalkParams-' + this.modelIndex)if (modelParams) {modelParams = JSON.parse(modelParams)params = {...params,...modelParams,modelName: name}}const res = await chatSubmit(params)const { code, obj } = res.dataif (code == 1) {this.chatId = objconst thinkIngTxt = this.$t('modelTalk.tips.thinkIng') //思考中……this.contentList.push({ type: 'user', message: this.inputMessage })this.historyList.push({ role: 'user', content: this.inputMessage })this.contentList.push({type: 'assistant',message: `<div class="think-time">${thinkIngTxt}</div>`})this.answerTitle =this.answerTitle || this.contentList[0].message.substring(0, 20)this.scrollToBottom()this.lastScrollHeight = 0this.connectSSE(obj)}},// 啟動連接connectSSE(chatId) {this.inputMessage = ''let buffer = ''let displayBuffer = ''this.startTime = nullthis.endTime = nullthis.thinkTime = nulllet len = this.contentList.lengthlet index = len % 2 === 0 ? len - 1 : lenlet historylen = this.historyList.lengthlet historyIndex = historylen % 2 === 0 ? historylen - 1 : historylenthis.isTalking = truelet anwerContent = ''this.connectionId = sse.connect({url: '/api/stream/chat',params: {chatId}},{onOpen: id => {console.log(`連接[${id}]已建立`)},onMessage: async (data, id) => {await this.sleep(10)try {var { content, finishReason } = data} catch (e) {console.log('e: ', e)}if (data && content) {let answerCont = contentbuffer += answerContanwerContent += answerContthis.$set(this.historyList, historyIndex, {role: 'assistant',content: anwerContent})const thinkIngTxt = this.$t('modelTalk.tips.thinkIng') //思考中……const deeplyPonderedTxt = this.$t('modelTalk.tips.deeplyPondered') //已深度思考// 單獨記錄時間if (answerCont.includes('<think>') ||answerCont.includes('</think>')) {// 執行替換邏輯if (answerCont.includes('<think>')) {answerCont = `<div class="think-time">${thinkIngTxt}</div><section id="think_content_${index}">`buffer = buffer.replaceAll('<think>', answerCont)this.startTime = Math.floor(new Date().getTime() / 1000)}if (answerCont.includes('</think>')) {answerCont = `</section>`this.endTime = Math.floor(new Date().getTime() / 1000)// 獲取到結束直接后,直接展示收起按鈕this.thinkTime = this.endTime - this.startTimebuffer = buffer.replaceAll(`<div class="think-time">${thinkIngTxt}</div>`,`<div class="think-time">${deeplyPonderedTxt}(${this.thinkTime}S)<i id="think_icon_${index}" onclick="hiddenThink(${index})" class="vxe-icon-arrow-down"></i></div>`).replaceAll('</think>', answerCont).replaceAll(`<section id="think_content_${index}"></section>`,'')}// 避免閃動 直接修改數據,這里不需要打字效果displayBuffer = buffer // 同步displayBuffer避免斷層this.$set(this.contentList, index, {type: 'assistant',message: this.markdownIt.render(buffer)})this.scrollToBottomIfAtBottom()} else {// 逐字效果if (!this.typingInterval) {this.typingInterval = setInterval(() => {if (displayBuffer.length < buffer.length) {const remaining = buffer.length - displayBuffer.length// 暫定一次性加3個字符const addChars = buffer.substr(displayBuffer.length,Math.min(3, remaining))displayBuffer += addCharslet markedText = this.markdownIt.render(displayBuffer)this.$set(this.contentList, index, {type: 'assistant',message: markedText})this.scrollToBottomIfAtBottom()} else {clearInterval(this.typingInterval)this.typingInterval = null}}, 40)}}} else {if (['stop', 'length'].includes(finishReason)) {this.scrollToBottomIfAtBottom()this.isTalking = falsethis.$emit('handleModelAnswerEnd', {modelIndex: this.modelIndex,contentList: this.contentList,finishReason: finishReason})}}},onError: (err, id) => {console.error(`連接[${id}]錯誤:`, err)},onFinalError: (err, id) => {console.log(`連接[${id}]已失敗`)}})},disconnectSSE() {sse.close()},async handleModelStop() {const res = await chatCancel({ chatId: this.chatId })const { code } = res.dataif (code == 1) {this.handleCleanOptionAndData()}},handleCleanOptionAndData() {this.disconnectSSE()this.isTalking = falsesetTimeout(() => {//清除強制停止的對話記錄this.historyList = this.historyList.slice(0, -2)}, 100)},scrollToBottom() {this.$nextTick(() => {const logContainer = document.querySelectorAll(`.chat-content-box .el-card__body`)[this.modelDomIndex]if (logContainer) {logContainer.scrollTop = logContainer.scrollHeight}})},scrollToBottomIfAtBottom() {this.$nextTick(() => {const logContainer = document.querySelectorAll(`.chat-content-box .el-card__body`)[this.modelDomIndex]if (!logContainer) returnconst threshold = 100const distanceToBottom =logContainer.scrollHeight -logContainer.scrollTop -logContainer.clientHeight// 獲取上次滾動位置const lastScrollHeight = this.lastScrollHeight || 0// 計算新增內容高度const deltaHeight = logContainer.scrollHeight - lastScrollHeight// 如果新增內容高度超過閾值50%,強制滾動if (deltaHeight > threshold / 2) {logContainer.scrollTop = logContainer.scrollHeight}// 否則正常滾動邏輯else if (distanceToBottom <= threshold) {logContainer.scrollTop = logContainer.scrollHeight}// 更新上次滾動位置記錄this.lastScrollHeight = logContainer.scrollHeight})/* logContainer.scrollTo({top: logContainer.scrollHeight,behavior: 'smooth'}) */},handleParamsConfig() {const modelRow = this.modelTypeList.find(item => item.id === this.modelType)this.$refs.ModelParamConfigRef.handleShow({...modelRow,modelIndex: this.modelIndex})}}
}
</script><style lang="scss" scoped>
.box-card {flex: 1;margin-bottom: 5px;height: 100%;display: flex;flex-direction: column;.header-item-box {display: flex;justify-content: space-between;align-items: center;}.chat-text-box {overflow: hidden;overflow-y: auto;}::v-deep .el-card__body {padding: 20px;flex: 1;overflow-y: auto;.talk-box {.talk-content {background-color: #fff;color: #324659;overflow-y: auto;box-sizing: border-box;padding: 0px 20px;.chat-assistant {display: flex;margin-bottom: 10px;.send-item {max-width: 60%;word-break: break-all;padding: 10px;background: #eef6ff;border-radius: 10px;color: #000000;white-space: pre-wrap;font-size: 13px;}.answer-item {line-height: 30px;color: #324659;}}.answer-cont {position: relative;display: flex;width: 100%;> img {width: 32px;height: 32px;margin-right: 10px;}&.end {justify-content: flex-end;}&.start {justify-content: flex-start;}}}.chat-sse {min-height: 100px;max-height: 460px;}.chat-message {height: calc(100vh - 276px);}.thinking-bubble {height: calc(100vh - 296px);}}.chat-add {width: 111px;height: 33px;background: #dbeafe;border-radius: 6px !important;font-size: 14px !important;border: 0px;color: #516ffe !important;&:hover {background: #ebf0f7;}.icon-tianjia1 {margin-right: 10px;font-size: 14px;}}.talk-btn-cont {text-align: right;height: 30px;margin-top: 5px;}}
}
</style>

創建主頁面index.vue

<template><div class="x-container-wrapper chat-page-box"><div class="chat-content-box"><template v-for="(item, i) in chatBoxs"><ChatBox:key="item.id"v-if="item.show":ref="el => setChatBoxRef(el, item.id)":modelTypeList="modelTypeList":modelIndex="item.id":modelDomIndex="getModelDomIndex(i)"@handleDeleteCurModel="handleDeleteCurModel"@handleModelAnswerEnd="handleModelAnswerEnd"></ChatBox></template></div><div class="middle-option-box"><vxe-buttontype="text"icon="iconfont icon-qingchu":disabled="hasAnsweringStatus"@click="handelAllHistoryAnswer"></vxe-button><vxe-buttontype="text"icon="vxe-icon-square-plus-square":disabled="hasAnsweringStatus"style="font-size: 24px"@click="handleAddModel"></vxe-button></div><div class="bottom-send-box"><div class="talk-send"><textarea@keydown="handleKeydown"ref="input"v-model="inputMessage"@input="adjustInputHeight":placeholder="$t('modelTalk.placeholder.sendMessage')":rows="2"/><div class="talk-btn-cont" style="text-align: right; font-size: 18px"><!-- 發送消息 --><vxe-buttonv-if="!hasAnsweringStatus"type="text"@click="sendMessage":disabled="sendBtnDisabled"icon="vxe-icon-send-fill"style="font-size: 24px"></vxe-button><!-- 停止回答 --><vxe-buttonv-elsetype="text"@click="handleModelStop"icon="vxe-icon-radio-checked"style="font-size: 24px"></vxe-button></div></div></div></div>
</template><script>
import 'highlight.js/styles/a11y-dark.css'
import { listModels } from '@/api/evaluationServer/modelTalk.js'
import ChatBox from '@/views/evaluationServer/modelTalk/components/ChatBox.vue'
import * as notify from '@/utils/notify'
export default {components: { ChatBox },data() {return {modelType: '',modelTypeList: [],inputMessage: '',eventSourceChat: null,answerTitle: '',thinkTime: null,startAnwer: false,startTime: null,endTime: null,typingInterval: null,msgHight: null,chatBoxs: [{ id: 1, content: '', show: true, isAnswerIng: false },{ id: 2, content: '', show: false, isAnswerIng: false },{ id: 3, content: '', show: false, isAnswerIng: false }]}},mounted() {this.getModelList()},methods: {async getModelList() {const params = { offset: 0, limit: '10000' }const res = await listModels(params)const { code, rows } = res.dataif (code == 1) {this.modelTypeList = rowsif (rows && rows.length) {this.modelType = rows[0].id}}},/* 清除提問和回答記錄 */handelAllHistoryAnswer() {this.chatBoxs.forEach(item => {item.content = ''const ref = this.$refs[`ChatBoxRef${item.id}`]if (ref && ref.clearHistory) {ref.clearHistory()}})notify.success(this.$t('modelTalk.tips.cleanrecorded'))},/* 增加模型窗口,最多三個 */handleAddModel() {const hasUnShow = this.chatBoxs.some(item => !item.show)if (hasUnShow) {const unShowRow = this.chatBoxs.filter(item => !item.show)if (unShowRow && unShowRow.length) {const index = this.chatBoxs.findIndex(item => item.id === unShowRow[0].id)this.chatBoxs[index].show = true}} else {notify.warning(this.$t('modelTalk.tips.maxModelNum3'))}},/* 獲取當前模型窗口位于第幾個dom */getModelDomIndex(i) {if (!i) return iconst hasShowModels = this.chatBoxs.filter(res => res.show)const hasShowLength = hasShowModels.lengthif (hasShowLength === 3) return iif (hasShowLength === 2) return 1},// enter鍵盤按下的換行賦值為空adjustInputHeight(event) {if (event.key === 'Enter' && !event.shiftKey) {this.inputMessage = ''event.preventDefault()return}this.$nextTick(() => {const textarea = this.$refs.inputtextarea.style.height = 'auto'// 最高200pxtextarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px'this.msgHight = textarea.style.height})},/* 如果按下Enter鍵 */handleKeydown(event) {if (event.isComposing) {return}if (event.key === 'Enter') {//Enter+shift 換行if (event.shiftKey) {return} else {// 按Enter,阻止默認行為,發送event.preventDefault()this.sendMessage()}}},/* 主動停止模型回答 */handleModelStop() {this.chatBoxs.forEach(item => {if (!item.isAnswerIng) returnconst ref = this.$refs[`ChatBoxRef${item.id}`]if (ref?.handleModelStop) {ref.handleModelStop().finally(() => {this.handleModelAnswerEnd({modelIndex: item.id,finishReason: 'stop'})})}})},/* 處理模型回答結束,更改回答狀態 */handleModelAnswerEnd(data) {const { modelIndex, finishReason } = data//stop正常響應結束,length輸出長度達到限制if (['stop', 'length'].includes(finishReason)) {this.$set(this.chatBoxs[modelIndex - 1], 'isAnswerIng', false)}},setChatBoxRef(el, id) {if (el) {this.$refs[`ChatBoxRef${id}`] = el} else {delete this.$refs[`ChatBoxRef${id}`]}},/* 發送消息 */sendMessage() {if (!this.inputMessage.trim()) returnif (!this.modelTypeList.length || !this.modelType) {//請選擇模型notify.warning(this.$t('modelTalk.tips.seleModel'))return}this.$nextTick(() => {this.chatBoxs.forEach((item, i) => {const ref = this.$refs[`ChatBoxRef${item.id}`]if (ref && ref.sendMessage) {this.chatBoxs[i].isAnswerIng = trueref.sendMessage({message: this.inputMessage,modelIndex: item.id})}})this.inputMessage = ''})},/* 刪除當前對話模型 */handleDeleteCurModel(index) {this.chatBoxs[index - 1].show = falseif (sessionStorage.getItem(`modelTalkParams-${index}`)) {sessionStorage.removeItem(`modelTalkParams-${index}`)}}},//清除sessionStorage中存儲的模型參數beforeDestroy() {this.chatBoxs.forEach(item => {sessionStorage.removeItem(`modelTalkParams-${item.id}`)})},computed: {//輸入框文本不為空,且不是回答中的狀態:發送按鈕可用sendBtnDisabled() {return !this.inputMessage.trim()},//存在回答中的狀態hasAnsweringStatus() {return this.chatBoxs.some(item => item.show && item.isAnswerIng)}}
}
</script><style lang="scss">
// 嘗試使用 @import 替代 @use 引入文件
@import '~@/assets/css/styles/chat-box-markdown.scss';
</style>
<style lang="scss" scoped>
.chat-page-box {display: flex;flex-direction: column;.chat-content-box {flex: 1;overflow: hidden;padding-top: 10px;display: grid;grid-template-columns: repeat(auto-fit, minmax(0, 1fr));gap: 10px;}
}
.middle-option-box {height: 30px;line-height: 30px;margin-top: 10px;::v-deep .vxe-button {.iconfont {font-size: 24px !important;}}
}
.bottom-send-box {width: 100%;min-height: 124px;padding: 10px 0;.talk-send {height: 100%;background: #f1f2f7;border-radius: 10px;border: 1px solid #e9e9eb;padding: 5px 10px;img {cursor: pointer;}textarea {width: 100%;padding: 10px;resize: none;overflow: auto;// min-height: 48px;height: 60px !important;line-height: 1.5;box-sizing: border-box;font-family: inherit;border: 0px;background: #f1f2f7;}textarea:focus {outline: none !important;}}
}
</style>

樣式scss

.hashrate-markdown {font-size: 14px;}.hashrate-markdown ol,.hashrate-markdown ul {padding-left: 2em;}.hashrate-markdown pre {border-radius: 6px;line-height: 1.45;overflow: auto;display: block;overflow-x: auto;background: #2c2c36;color: rgb(248, 248, 242);padding: 16px 8px;}.hashrate-markdown h1,.hashrate-markdown h2,.hashrate-markdown h3 {// font-size: 1em;}.hashrate-markdown h4,.hashrate-markdown h5,.hashrate-markdown h6 {font-weight: 600;line-height: 1.7777;margin: 0.57142857em 0;}.hashrate-markdown li {margin: 0.5em 0;}.hashrate-markdown strong {font-weight: 600;}.hashrate-markdown p {white-space: pre-wrap;word-break: break-word;line-height: 24px;color: #324659;font-size: 14px;}.hashrate-markdown hr {background-color: #e8eaf2;border: 0;box-sizing: content-box;height: 1px;margin: 12px 0;min-width: 10px;overflow: hidden;padding: 0;}.hashrate-markdown table {border-collapse: collapse;border-spacing: 0;display: block;max-width: 100%;overflow: auto;width: max-content;}.hashrate-markdown table tr {border-top: 1px solid #e8eaf2;}.hashrate-markdown table td,.hashrate-markdown table th {border: 1px solid #e8eaf2;padding: 6px 13px;}.hashrate-markdown table th {background-color: #f3f2ff;font-weight: 600;}.hashrate-markdown section {margin-inline-start: 0px;border-left: 2px solid #e5e5e5;padding-left: 10px;color: #718096;margin-bottom: 5px;font-size: 12px;p {color: #718096;font-size: 12px;margin: 8px 0;}}.think-time {height: 36px;background: #f1f2f7;border-radius: 10px;line-height: 36px;font-size: 12px;display: inline-flex;padding: 0px 15px;margin-bottom: 20px;color: #1e1e1e;>i{line-height: 36px;margin-left: 5px;}}

封裝sse.js

import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getToken } from '@/utils/auth' // 假設從auth工具獲取tokenclass SSEService {constructor() {this.connections = new Map() // 存儲所有連接 { connectionId: { controller, config } }this.DEFAULT_ID_PREFIX = 'sse_conn_'this.MAX_RETRIES = 3 // 最大自動重試次數this.BASE_RETRY_DELAY = 1000 // 基礎重試延遲(ms)// 默認請求頭(可通過setDefaultHeaders動態更新)this.DEFAULT_HEADERS = {'Content-Type': 'application/json','X-Auth-Token': getToken() || ''}}/*** 創建SSE連接* @param {Object} config - 連接配置* @param {String} config.url - 接口地址* @param {'GET'|'POST'} [config.method='GET'] - 請求方法* @param {Object} [config.params={}] - 請求參數* @param {Object} [config.headers={}] - 自定義請求頭* @param {String} [config.connectionId] - 自定義連接ID* @param {Object} handlers - 事件處理器* @returns {String} connectionId*/connect(config = {}, handlers = {}) {const connectionId = config.connectionId || this._generateConnectionId()this.close(connectionId)// 合并headers(自定義優先)const headers = {...this.DEFAULT_HEADERS,...(config.headers || {}),'X-Auth-Token': getToken() || '' // 確保token最新}// 構建請求配置const requestConfig = {method: config.method || 'GET',headers,signal: this._createController(connectionId),openWhenHidden: true // 頁面隱藏時保持連接}// 處理URL和參數const requestUrl = this._buildUrl(config.url,config.params,requestConfig.method)if (requestConfig.method === 'POST') {requestConfig.body = JSON.stringify(config.params || {})}// 存儲連接信息this.connections.set(connectionId, {config: { ...config, connectionId },controller: requestConfig.signal.controller,retryCount: 0})// 發起連接this._establishConnection(requestUrl, requestConfig, connectionId, handlers)return connectionId}/*** 實際建立連接(含自動重試邏輯)*/async _establishConnection(url, config, connectionId, handlers) {const connection = this.connections.get(connectionId)try {await fetchEventSource(url, {...config,onopen: async response => {if (response.ok) {connection.retryCount = 0handlers.onOpen?.(connectionId)} else {throw new Error(`SSE連接失敗: ${response.status}`)}},onmessage: msg => {try {const data = msg.data ? JSON.parse(msg.data) : nullhandlers.onMessage?.(data, connectionId)} catch (err) {handlers.onError?.(err, connectionId)}},onerror: err => {if (connection.retryCount < this.MAX_RETRIES) {const delay =this.BASE_RETRY_DELAY * Math.pow(2, connection.retryCount)setTimeout(() => {connection.retryCount++this._establishConnection(url, config, connectionId, handlers)}, delay)} else {handlers.onFinalError?.(err, connectionId)this.close(connectionId)}throw err // 阻止庫默認的重試邏輯}})} catch (err) {console.error(`[SSE ${connectionId}] 連接異常:`, err)}}/*** 關閉指定連接*/close(connectionId) {const conn = this.connections.get(connectionId)if (conn?.controller) {conn.controller.abort()this.connections.delete(connectionId)}}/*** 關閉所有連接*/closeAll() {this.connections.forEach(conn => conn.controller?.abort())this.connections.clear()}/*** 更新默認請求頭*/setDefaultHeaders(headers) {this.DEFAULT_HEADERS = { ...this.DEFAULT_HEADERS, ...headers }}// -------------------- 工具方法 --------------------_generateConnectionId() {return `${this.DEFAULT_ID_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 7)}`}_buildUrl(baseUrl, params = {}, method) {const url = new URL(baseUrl, window.location.origin)if (method === 'GET' && params) {Object.entries(params).forEach(([key, value]) => {if (value !== undefined) url.searchParams.set(key, value)})}return url.toString()}_createController(connectionId) {const controller = new AbortController()const conn = this.connections.get(connectionId)if (conn) conn.controller = controllerreturn controller.signal}
}export const sse = new SSEService()

?sse響應數據格式

{"content":"<think>","reasoningContent":null,"created":1754036279,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"8b593756-2aeb-416e-9839-645abb8dfcef"}
{"content":"我是","reasoningContent":null,"created":1754036279,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"8b593756-2aeb-416e-9839-645abb8dfcef"}
{"content":"Deep","reasoningContent":null,"created":1754036279,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"8b593756-2aeb-416e-9839-645abb8dfcef"}
{"content":"</think>","reasoningContent":null,"created":1754036930,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}
{"content":"我可以","reasoningContent":null,"created":1754036930,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}
{"content":"理解","reasoningContent":null,"created":1754036930,"finishReason":null,"modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}
{"content":"","reasoningContent":null,"created":1754036930,"finishReason":"stop","modelName":"DS-1.5B","deployId":null,"id":"0548d17e-72b5-4219-857d-054155d59096"}

ModelParamConfig組件屬于參數配置表單,可根據實際需求開發

最終效果

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

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

相關文章

電腦上不了網怎么辦?【圖文詳解】wifi有網絡但是電腦連不上網?網絡設置

一、問題背景 你有沒有遇到過這種情況&#xff1a;電腦右下角的網絡圖標明明顯示連接正常&#xff0c;可打開瀏覽器就是加載不出網頁&#xff0c;聊天軟件也刷不出新消息&#xff1f; 這種 "網絡已連接但無法上網" 的問題特別常見&#xff0c;既不是沒插網線&#xf…

思途Spring學習 0804

SpringBoot 核心概念與開發實踐SpringBoot 是一個基于 Spring 框架的快速開發腳手架&#xff0c;通過約定大于配置的原則簡化了傳統 Spring 應用的初始化配置。其核心目標是整合 Spring 生態&#xff08;如 SSM&#xff09;并支持微服務架構開發。控制反轉&#xff08;IoC&…

Hutool工具類:Java開發必備神器

Hutool工具類使用說明Hutool是一個Java工具類庫&#xff0c;提供了豐富的功能模塊&#xff0c;包括字符串處理、日期時間操作、IO流、加密解密、HTTP客戶端等。以下是一些常用模塊的具體使用方法。字符串工具&#xff08;StrUtil&#xff09;字符串處理是開發中的常見需求&…

Node.js中Buffer的用法

// Buffer 與字符串的轉換示例 // Buffer 是 Node.js 中用于處理二進制數據的類&#xff0c;字符串與 Buffer 之間的轉換是常見操作// 1. 從字節數組創建 Buffer 并轉換為字符串 // Buffer.from(array) 接收一個字節數值數組&#xff0c;創建對應的 Buffer let buf_4 Buffer.f…

【Java 基礎】Java 源代碼加密工具有哪些?

??博主介紹: 博主從事應用安全和大數據領域,有8年研發經驗,5年面試官經驗,Java技術專家,WEB架構師,阿里云專家博主,華為云云享專家,51CTO 專家博主 ?? 個人社區:個人社區 ?? 個人主頁:個人主頁 ?? 專欄地址: ? Java 中級 ??八股文專題:劍指大廠,手撕 J…

es的histogram直方圖聚合和terms分組聚合

你提到的這兩個 Elasticsearch aggs 聚合語句&#xff1a;第一種&#xff1a;histogram 直方圖聚合 "aggs": {"DayDiagram": {"histogram": {"field": "${FiledName}","interval": ${TimeInterval},"extende…

基于Java的AI/機器學習庫(Smile、Weka、DeepLearning4J)的實用

基于Java和AI技術處理動漫視頻 以下是一些基于Java和AI技術處理動漫視頻(如《亞久斗》)的實用案例和實現方法,涵蓋視頻分析、風格轉換、角色識別等方向。每個案例均提供技術思路和關鍵代碼片段。 視頻關鍵幀提取 使用OpenCV提取動漫視頻中的關鍵幀,保存為圖片供后續分析…

筆記本電腦聯想T14重啟后無法識別外置紅米屏幕

【原先是可以連接重啟后不行】按照以下步驟排查和解決&#xff1a;? 1. 基礎排查確認連接方式&#xff1a;檢查是否使用 USB-C轉DP/HDMI線 或 HDMI/DP直連&#xff0c;嘗試更換線纜或接口&#xff08;如換另一個USB-C口或HDMI口&#xff09;。測試顯示器&#xff1a;將紅米顯示…

vue+ts 基礎面試題 (一 )

目錄 1.Vue3 響應式原理 一、 響應式的基本概念 二、 核心機制&#xff1a;Proxy 和依賴追蹤 三、 觸發更新的過程 四、 代碼示例 五、 優勢總結 2.如何實現組件間通信&#xff1f; 一、父子組件通信 1. 父傳子&#xff1a;Props 傳遞 2. 子傳父&#xff1a;自定義事…

Spring AI實戰:SpringBoot項目結合Spring AI開發——提示詞(Prompt)技術與工程實戰詳解

&#x1fa81;&#x1f341; 希望本文能給您帶來幫助&#xff0c;如果有任何問題&#xff0c;歡迎批評指正&#xff01;&#x1f405;&#x1f43e;&#x1f341;&#x1f425; 文章目錄一、前言二、提示詞前置知識2.1 提示詞要素2.2 設計提示詞的通用技巧2.2.1 從簡單開始2.2.…

【后端】Java static 關鍵字詳解

在 Java 中&#xff0c;static 是一個修飾符&#xff0c;用于定義與類相關&#xff08;而非對象實例相關&#xff09;的成員。以下是核心知識點和用法&#xff1a;一、四大用途靜態變量&#xff08;類變量&#xff09; 作用&#xff1a;屬于類&#xff0c;而非實例。所有實例共…

算法訓練營DAY50 第十一章:圖論part01

98. 所有可達路徑 98. 所有可達路徑 【題目描述】 給定一個有 n 個節點的有向無環圖&#xff0c;節點編號從 1 到 n。請編寫一個程序&#xff0c;找出并返回所有從節點 1 到節點 n 的路徑。每條路徑應以節點編號的列表形式表示。 【輸入描述】 第一行包含兩個整數 N&#…

OpenCV:從入門到實戰的全方位指南

目錄 一、OpenCV 簡介 &#xff08;一&#xff09;特點 &#xff08;二&#xff09;應用場景 二、OpenCV 的核心模塊 &#xff08;一&#xff09;core 模塊 &#xff08;二&#xff09;imgproc 模塊 &#xff08;三&#xff09;video 模塊 &#xff08;四&#xff09;f…

如何在 Ubuntu 24.04 上安裝和配置 TFTP 服務器

了解如何在 Ubuntu 24.04 Linux 上安裝 TFTP 以執行基本的文件傳輸。 簡單文件傳輸協議(TFTP)是標準 FTP 的輕量級替代方案,用于在聯網設備之間傳輸文件。與 FTP 和 HTTP 相比,TFTP 更簡單,無需復雜的客戶端-服務器模型即可操作。這就是為什么該協議用于執行基本文件傳輸…

基于 AXI-Lite 實現可擴展的硬件函數 RPC 框架(附完整源碼)

AXI-Lite 實現RPC調用硬件函數服務 &#x1f44b; 本文介紹如何基于 AXI-Lite 總線設計一個通用的“硬件函數調用框架”。主機端&#xff08;PS&#xff09;只需通過寄存器寫入參數與啟動標志&#xff0c;即可觸發 PL 模塊執行指定算法邏輯&#xff0c;并將結果返回。 該機制本…

[spring-cloud: NamedContextFactory ClientFactoryObjectProvider]-源碼閱讀

依賴 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-commons</artifactId><version>4.3.0</version> </dependency>源碼 NamedContextFactory NamedContextFactory 類通過創建多個子…

HBase MOB技術特點及使用場景介紹

在 HBase 2.0 版本之前,雖然 HBase 能夠存儲從 1 字節到 10MB 大小的二進制對象 ,但其讀寫路徑主要針對小于 100KB 的值進行了優化。當面對大量大小在 100KB - 10MB 之間的數據時,傳統的存儲方式就會暴露出問題。例如,當存儲大量的圖片、文檔或短視頻等中等大小對象時,由于…

Ubuntu 配置密鑰+密碼登錄

目錄 1、密鑰生成 2、發送公鑰至 需要連接的服務器 3、選用私鑰登錄 1、密鑰生成 ssh-keygen -t rsa -b 4096 -C "angindem"2、發送公鑰至 需要連接的服務器 將.ssh中的id_rsa.pub 的密鑰&#xff0c;放在authorized_keys中 注意&#xff1a;.ssh 文件夾一定賦予…

谷歌瀏覽器Chrome 緩存遷移

步驟 1&#xff1a;準備數據遷移1. 關閉 Chrome 及所有后臺進程在任務管理器&#xff08;CtrlShiftEsc&#xff09;中結束所有 chrome.exe 進程。 2. 備份并移動原數據- 將 C:\Users\xxx\AppData\Local\Google\Chrome\User Data **整個文件夾**復制到新位置&#xff08;如 G:\…

Java中的RabbitMQ完全指南

Java中的RabbitMQ完全指南 1. 引言 什么是RabbitMQ RabbitMQ是一個開源的消息代理和隊列服務器&#xff0c;實現了高級消息隊列協議&#xff08;AMQP&#xff09;。它充當應用程序之間的消息中間件&#xff0c;允許分布式系統中的不同組件進行異步通信。RabbitMQ使用Erlang語言…