| | |
| | | <template> |
| | | <div style="position: relative;height:calc(100vh - 20px);width: 100%;display: flex;justify-content: center"> |
| | | <div style="height:calc(100vh - 20px);width: 100%;display: flex;justify-content: center"> |
| | | <!-- 聊天消息列表 --> |
| | | <div class="chat-messages"> |
| | | <div |
| | |
| | | <div class="avatar"> |
| | | <img :src="getAvatar(message.role)" alt="avatar" /> |
| | | </div> |
| | | <div class="content"> |
| | | <div class="text" :ref="'msg' + index">{{ message.content }}</div> |
| | | <div v-if="!message.content && message.role == 'assistant'" v-loading="!message.content && message.role == 'assistant'"> |
| | | |
| | | </div> |
| | | <div v-else class="content"> |
| | | <div class="text" :ref="'msg' + index" v-html="getHtml(message.content)"></div> |
| | | <div v-if="message.role === 'assistant'"> |
| | | <div v-show="msgIndex === message" class="msg-op-list"> |
| | | <el-button class="copy" v-show="false"/> |
| | |
| | | |
| | | <!-- 输入框 --> |
| | | <div class="chat-input"> |
| | | <div style="position: relative;width: 100%;box-sizing: border-box;"> |
| | | <div style="position: relative;width: 800px;box-sizing: border-box;"> |
| | | <el-input |
| | | v-model="inputMessage" |
| | | type="textarea" |
| | |
| | | </div> |
| | | </div> |
| | | <div class="send-but-warp"> |
| | | <div class="send-but"> |
| | | <div class="send-but" @click="sendMessage"> |
| | | <svg t="1743144485359" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1261" width="36" height="36"> |
| | | <path fill="#409eff" d="M512 85.333333c234.666667 0 426.666667 192 426.666667 426.666667s-192 426.666667-426.666667 426.666667S85.333333 746.666667 85.333333 512 277.333333 85.333333 512 85.333333z m-6.4 234.666667c-4.266667 2.133333-6.4 2.133333-12.8 8.533333l-153.6 153.6c-12.8 12.8-12.8 32 0 44.8 12.8 12.8 32 12.8 44.8 0l96-96V682.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V430.933333l96 96c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8l-153.6-153.6c-6.4-6.4-8.533333-8.533333-12.8-8.533333s-8.533333-2.133333-12.8 0z" p-id="1262"></path> |
| | | </svg> |
| | |
| | | |
| | | <script> |
| | | import ClipboardJS from 'clipboard'; |
| | | import {sendKbMsg} from "@/api/chat"; |
| | | import {Message} from "element-ui"; |
| | | const markdownIt = require('markdown-it')(); |
| | | |
| | | export default { |
| | | name: 'AiChat', |
| | | data() { |
| | | return { |
| | | chatId: null, |
| | | messages: [], // 用于页面展示的对话列表 |
| | | netSearchEnable: false, |
| | | messages: [ |
| | | { |
| | | role: 'assistant', |
| | | content: '你好!我是你的 AI 助手,有什么可以帮你的吗?', |
| | | }, |
| | | { |
| | | role: 'user', |
| | | content: '你好!我是你的 AI 助手,有什么可以帮你的吗?', |
| | | }, |
| | | ], |
| | | inputMessage: '', |
| | | sendMsgForm: { |
| | | query: "", |
| | | mode: "local_kb", |
| | | kb_name: "samples", |
| | | top_k: 3, |
| | | score_threshold: 2, |
| | | kbName: "SouthWest_Neclear_Develepment_KB", |
| | | topK: 3, |
| | | scoreThreshold: 2, |
| | | history: [ |
| | | { |
| | | "content": "我们来玩成语接龙,我先来,生龙活虎", |
| | | "role": "user" |
| | | }, |
| | | { |
| | | "content": "虎头虎脑", |
| | | "role": "assistant" |
| | | } |
| | | |
| | | ], |
| | | stream: true, |
| | | model: "qwen:7b", |
| | | temperature: 0.7, |
| | | max_tokens: 0, |
| | | prompt_name: "default", |
| | | return_direct: false |
| | | model: "Qwen25-32B-Instruct", |
| | | temperature: 1.15, |
| | | maxTokens: 512, |
| | | promptName: "default", |
| | | returnDirect: false |
| | | }, |
| | | msgIndex: null, |
| | | msgRole: null |
| | | msgRole: null, |
| | | chunkRef: null, // ai回答临时数据 |
| | | haveThinkStart: false, // ai回答响应数据是否有think标签 |
| | | haveThinkEnd: false, // ai回答响应数据是否有think标签 |
| | | controller: null, // ai回答请求控制器,可以根据业务中断请求 |
| | | }; |
| | | }, |
| | | methods: { |
| | | // md渲染为html |
| | | getHtml(md) { |
| | | return markdownIt.render(md) |
| | | }, |
| | | changeNetEnable() { |
| | | this.netSearchEnable = ! this.netSearchEnable |
| | | }, |
| | |
| | | e.preventDefault(); // 阻止默认换行行为 |
| | | }, |
| | | handleMouseEnter(msgIndex) { |
| | | console.log("鼠标移入", msgIndex) |
| | | this.msgIndex = msgIndex |
| | | }, |
| | | handleMouseLeave() { |
| | | console.log("鼠标移除") |
| | | this.msgIndex = null |
| | | }, |
| | | // 重新生成 |
| | |
| | | return roleNames[role]; |
| | | }, |
| | | |
| | | // 发送消息 |
| | | async sendMessage() { |
| | | if (this.inputMessage.trim() === '') return; |
| | | |
| | | // 添加用户消息 |
| | | this.messages.push({ |
| | | role: 'user', |
| | | content: this.inputMessage, |
| | | }); |
| | | this.sendMsgForm.query = this.inputMessage |
| | | // 清空输入框 |
| | | this.inputMessage = ''; |
| | | // 初始化状态 |
| | | this.chunkRef = ''; |
| | | this.haveThinkStart = false; |
| | | this.haveThinkEnd = false; |
| | | this.controller = new AbortController(); |
| | | |
| | | sendKbMsg(this.sendMsgForm).then(res => { |
| | | console.log(res, "拿到回复了!!") |
| | | this.messages.push({ |
| | | role: 'assistant', |
| | | content: '这是一条 AI 回复。', |
| | | // 添加用户消息和占位助理消息 |
| | | this.messages.push( |
| | | { role: 'user', content: this.inputMessage }, |
| | | { role: 'assistant', content: '' } |
| | | ); |
| | | this.sendMsgForm.query = this.inputMessage; |
| | | this.inputMessage = ''; |
| | | const assistantIndex = this.messages.length - 1; |
| | | try { |
| | | const response = await fetch('/api/chat/send/msg', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | "Accept": 'text/event-stream', |
| | | 'Cache-Control': 'no-cache, no-transform' |
| | | }, |
| | | signal: this.controller.signal, |
| | | body: JSON.stringify(this.sendMsgForm) |
| | | }); |
| | | }) |
| | | }, |
| | | |
| | | if (!response.ok) throw new Error(`HTTP ${response.status}`); |
| | | |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let jsonBuffer = ''; |
| | | |
| | | // eslint-disable-next-line no-constant-condition |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) { |
| | | this.sendMsgForm.history.push({ role: 'user', content: this.inputMessage }) |
| | | this.sendMsgForm.history.push({ role: 'assistant', content: this.messages[assistantIndex].content }) |
| | | break; |
| | | } |
| | | |
| | | const chunk = decoder.decode(value, { stream: true }); |
| | | const lines = chunk.split('\n'); |
| | | |
| | | for (const line of lines) { |
| | | if (!line.startsWith('data:')) continue; |
| | | |
| | | const rawData = line.slice(5).trim(); |
| | | if (rawData === '[DONE]') continue; |
| | | |
| | | try { |
| | | jsonBuffer += rawData; |
| | | const data = JSON.parse(jsonBuffer); |
| | | jsonBuffer = ''; |
| | | let content = ''; |
| | | if (data.docs) { |
| | | for (let i = 0; i < data.docs.length; i++) { |
| | | content += data.docs[i] |
| | | } |
| | | } else { |
| | | content = data?.choices?.[0]?.delta?.content || ''; |
| | | } |
| | | if (!content) continue; |
| | | // 实时更新 |
| | | this.messages[assistantIndex].content += content; |
| | | this.messages[assistantIndex].content = this.messages[assistantIndex].content.replace(/<\/?think>/g, ''); |
| | | await this.$nextTick(); |
| | | } catch { |
| | | // JSON不完整,等待下次数据 |
| | | continue; |
| | | } |
| | | } |
| | | } |
| | | } catch (error) { |
| | | if (error.name !== 'AbortError') { |
| | | console.error('Request failed:', error); |
| | | this.messages[assistantIndex].content = '请求异常'; |
| | | } |
| | | } |
| | | } |
| | | |
| | | }, |
| | | }; |
| | | </script> |
| | |
| | | .chat-messages { |
| | | padding: 16px; |
| | | margin-top: 14px; |
| | | overflow-y: auto; |
| | | width: 800px; |
| | | height: 680px; |
| | | } |
| | | |
| | | .message { |
| | |
| | | } |
| | | |
| | | .chat-input { |
| | | padding: 16px; |
| | | background-color: #fff; |
| | | width: 800px; |
| | | width: 100%; |
| | | display: flex; |
| | | flex-direction: row; |
| | | justify-content: center; |