当前位置: 首页 > news >正文

vue实现模拟deepseekAI功能

       如果是get可以考虑用sse,我当前是post,采用的是post长轮询实现

   /*** 发送用户消息并与AI进行流式对话交互。* 该函数会创建一个机器人回复的消息对象,并将其推入消息列表中。* 随后通过fetch向后端发起聊天请求,支持SSE(Server-Sent Events)或普通JSON响应,* 并将返回的内容逐步追加到机器人的回复内容中,实现打字机效果。** @param {string} message - 用户输入的原始消息内容*/

代码实现

<template><div class="chat-dialog"><!-- 聊天消息区域 --><div class="chat-messages" ref="messagesContainer"><divv-for="(message, index) in messages":key="index":class="['message', message.type]"><!-- 机器人头像 --><div v-if="message.type === 'bot'" class="avatar bot-avatar"><div class="robot-icon">🤖</div></div><!-- 消息内容 --><div class="message-content"><divclass="message-bubble":class="[message.type,{loading:isLoading &&message.type === 'bot' &&!(message.content || message.displayContent)}]"><divv-if="isLoading &&message.type === 'bot' &&!(message.content || message.displayContent)"class="typing-indicator"><span></span><span></span><span></span></div><div v-else><divv-if="message.displayContent"v-text="message.displayContent"></div><div v-else v-html="formatMessage(message.content)"></div></div></div></div><!-- 用户头像 --><div v-if="message.type === 'user'" class="avatar user-avatar"><div class="user-icon">👤</div></div></div></div><!-- 输入区域 --><div class="chat-input-area"><div class="input-container"><inputv-model="inputMessage"type="text":placeholder="placeholderText"class="chat-input"@keyup.enter="sendMessage":disabled="isLoading"/><buttonclass="send-button"@click="sendMessage":disabled="!inputMessage.trim() || isLoading"><svg width="16" height="16" viewBox="0 0 24 24" fill="none"><pathd="M2.01 21L23 12L2.01 3L2 10L17 12L2 14L2.01 21Z"fill="white"/></svg></button></div><div class="chat-actions"><buttonclass="reset-button"@click="resetConversation":disabled="isLoading">重置对话</button></div></div></div>
</template><script>
import { generateSessionId, formatSSEMessage } from "@/utils/sseUtils";
import settings from "@/settings";export default {name: "ChatDialog",props: {placeholderText: {type: String,default: "和云浮局-分布式电源接入对台区重过载预警智能体聊天"}},data() {return {messages: [],inputMessage: "",isLoading: false,conversationId: generateSessionId(),parentMessageId: null,currentAbortController: null,pollInterval: null,typingDelayMs: 10};},methods: {async sendMessage() {if (!this.inputMessage.trim() || this.isLoading) return;const userMessage = {type: "user",content: this.inputMessage.trim(),timestamp: new Date()};this.messages.push(userMessage);const currentMessage = this.inputMessage.trim();this.inputMessage = "";await this.$nextTick();this.scrollToBottom();this.isLoading = true;try {await this.sendChatWithPolling(currentMessage);} catch (error) {console.error("发送消息失败:", error);this.messages.push({type: "bot",content: "抱歉,我暂时无法回复您的消息,请稍后再试。",timestamp: new Date()});} finally {this.isLoading = false;await this.$nextTick();this.scrollToBottom();}},async sendChatWithPolling(message) {console.log("使用长轮询方式发送聊天请求:", message);/*** 发送用户消息并与AI进行流式对话交互。* 该函数会创建一个机器人回复的消息对象,并将其推入消息列表中。* 随后通过fetch向后端发起聊天请求,支持SSE(Server-Sent Events)或普通JSON响应,* 并将返回的内容逐步追加到机器人的回复内容中,实现打字机效果。** @param {string} message - 用户输入的原始消息内容*/const botMessage = {type: "bot",content: "",displayContent: "",timestamp: new Date()};this.messages.push(botMessage);try {// 生成当前消息ID并清理用户输入内容const currentMessageId = generateSessionId();const cleanMessage = message.trim();// 检查消息是否为空if (!cleanMessage) {botMessage.content = "消息内容不能为空";return;}// 构造请求数据const requestData = {response_mode: "streaming",conversation_id: this.conversationId,files: [],query: cleanMessage,inputs: {},parent_message_id: this.parentMessageId || currentMessageId// conversation_id: "ee87ad3c-bde6-4d74-88bf-ba74d99c0974",// query: "什么是“重过载”?",// parent_message_id: "2cc5088a-24f6-4d5f-b2b4-9a6f9e1b3ad4"};// 更新父级消息IDthis.parentMessageId = currentMessageId;// 创建用于取消请求的控制器this.currentAbortController = new AbortController();const signal = this.currentAbortController.signal;let cursor = null;let finished = false;// 开始长轮询处理流式响应while (!finished) {const payload = { ...requestData, cursor };try {// 向AI接口发送POST请求const res = await fetch(settings.aiPrefix + "/chat", {method: "POST",headers: { "Content-Type": "application/json" },body: JSON.stringify(payload),signal});// 请求失败则抛出错误if (!res.ok) throw new Error(`HTTP ${res.status}`);// 获取响应类型与文本内容const contentType = res.headers.get("content-type") || "";const rawText = await res.text();let events = [];// 判断是SSE格式还是普通JSON格式if (contentType.includes("text/event-stream") ||rawText.startsWith("data:")) {// 处理SSE事件流:按行分割并解析每条消息const lines = rawText.split(/\r?\n/);console.log("lines", lines);for (const line of lines) {if (!line || !line.startsWith("data:")) continue;const jsonStr = line.slice(5).trim();if (!jsonStr) continue;try {events.push(JSON.parse(jsonStr));} catch (_) {}}} else {// 尝试作为单个JSON对象解析try {events = [JSON.parse(rawText)];} catch (_) {events = [];}}// 遍历所有事件并更新bot消息内容for (let i = 0; i < events.length; i += 1) {const evt = events[i];const answerVal =(evt && evt.data && evt.data.answer) || (evt && evt.answer);// console.log("处理事件:", evt);// console.log("提取的answerVal:", answerVal);if (typeof answerVal === "string" && answerVal) {// console.log("开始打字效果处理answerVal:", answerVal);await this.appendWithTyping(botMessage, answerVal);}// 更新游标和结束标志位(基于当前事件)cursor = (evt && (evt.cursor || evt.next_cursor)) || cursor;if ((evt && (evt.done || evt.is_end || evt.finished)) ||(evt && evt.event === "workflow_finished") ||(evt && evt.event === "done") ||(evt && evt.event === "error")) {finished = true;}}// 触发视图更新并滚动到底部await this.$nextTick();this.scrollToBottom();// 若未完成且无新事件及游标,则短暂等待避免频繁请求if (!finished && events.length === 0 && !cursor) {await new Promise((r) => setTimeout(r, 200));}} catch (err) {// 中断请求时退出循环if (err && err.name === "AbortError") break;console.error("长轮询失败:", err);botMessage.content += "\n请求失败,请稍后重试。";break;}}} catch (error) {// 兜底异常处理console.error("发送消息失败:", error);botMessage.content += "\n\n抱歉,我暂时无法回复您的消息,请稍后再试。";}},resetConversation() {this.messages = [];this.conversationId = generateSessionId();this.parentMessageId = null;this.messages.push({type: "bot",content:"您好! 😊 我是云浮局分布式电源接入对台区重过载预警智能体,有什么可以帮助您的吗?",timestamp: new Date()});},formatMessage(content) {return formatSSEMessage(content);},scrollToBottom() {const el = this.$refs.messagesContainer;if (el) el.scrollTop = el.scrollHeight;},async appendWithTyping(botMessage, text) {if (!text) return;// console.log("开始打字效果,文本:", text);// 累积到真实内容(已预定义属性,直接赋值保持响应)botMessage.content = (botMessage.content || "") + text;// 按字符逐个展现for (let i = 0; i < text.length; i += 1) {botMessage.displayContent = (botMessage.displayContent || "") + text[i];// console.log("当前显示内容:", botMessage.displayContent);// 等待一小段时间,形成打字效果// eslint-disable-next-line no-await-in-loopawait new Promise((r) => setTimeout(r, this.typingDelayMs));// eslint-disable-next-line no-await-in-loopawait this.$nextTick();// 保险触发一次刷新,避免个别环境下不重绘if (this.$forceUpdate) this.$forceUpdate();this.scrollToBottom();}}},mounted() {this.messages.push({type: "bot",content:"您好! 😊 我是云浮局分布式电源接入对台区重过载预警智能体,有什么可以帮助您的吗?",timestamp: new Date()});},beforeDestroy() {if (this.currentAbortController) {this.currentAbortController.abort();this.currentAbortController = null;}if (this.pollInterval) {clearInterval(this.pollInterval);this.pollInterval = null;}}
};
</script><style scoped>
.chat-dialog {display: flex;flex-direction: column;height: 600px;background-color: #f8f9fa;border-radius: 8px;overflow: hidden;
}.chat-messages {flex: 1;overflow-y: auto;padding: 20px;background-color: #f8f9fa;
}.message {display: flex;margin-bottom: 20px;align-items: flex-start;
}.message.user {flex-direction: row-reverse;
}.message-content {max-width: 70%;margin: 0 10px;
}.message-bubble {padding: 12px 16px;border-radius: 18px;word-wrap: break-word;line-height: 1.4;white-space: pre-wrap; /* 保留换行,便于打字中显示 */
}.message-bubble.user {background-color: #e3f2fd;color: #1976d2;border-bottom-right-radius: 4px;
}.message-bubble.bot {background-color: white;color: #333;border-bottom-left-radius: 4px;box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}.avatar {width: 40px;height: 40px;border-radius: 50%;display: flex;align-items: center;justify-content: center;flex-shrink: 0;
}.bot-avatar {background: linear-gradient(135deg, #4caf50, #2e7d32);
}.user-avatar {background: linear-gradient(135deg, #2196f3, #1976d2);
}.robot-icon,
.user-icon {font-size: 20px;color: white;
}.chat-input-area {padding: 20px;background-color: white;border-top: 1px solid #e9ecef;
}.chat-actions {margin-top: 10px;display: flex;justify-content: flex-end;
}.reset-button {padding: 8px 16px;border: 1px solid #dc3545;border-radius: 6px;background-color: white;color: #dc3545;cursor: pointer;font-size: 14px;transition: all 0.2s;
}.reset-button:hover:not(:disabled) {background-color: #dc3545;color: white;
}.reset-button:disabled {opacity: 0.6;cursor: not-allowed;
}.test-button {padding: 8px 16px;border: 1px solid #28a745;border-radius: 6px;background-color: white;color: #28a745;cursor: pointer;font-size: 14px;transition: all 0.2s;margin-left: 10px;
}.test-button:hover:not(:disabled) {background-color: #28a745;color: white;
}.test-button:disabled {opacity: 0.6;cursor: not-allowed;
}.compare-button {padding: 8px 16px;border: 1px solid #ffc107;border-radius: 6px;background-color: white;color: #ffc107;cursor: pointer;font-size: 14px;transition: all 0.2s;margin-left: 10px;
}.compare-button:hover:not(:disabled) {background-color: #ffc107;color: white;
}.compare-button:disabled {opacity: 0.6;cursor: not-allowed;
}.input-container {display: flex;align-items: center;background-color: white;border-radius: 25px;padding: 8px 16px;box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}.chat-input {flex: 1;border: none;outline: none;padding: 12px 0;font-size: 14px;background: transparent;
}.chat-input::placeholder {color: #999;
}.send-button {width: 36px;height: 36px;border: none;border-radius: 50%;background: linear-gradient(135deg, #2196f3, #1976d2);color: white;cursor: pointer;display: flex;align-items: center;justify-content: center;transition: all 0.2s;box-shadow: 0 2px 4px rgba(33, 150, 243, 0.3);
}.send-button:hover:not(:disabled) {background: linear-gradient(135deg, #1976d2, #1565c0);transform: translateY(-1px);box-shadow: 0 4px 8px rgba(33, 150, 243, 0.4);
}.send-button:disabled {background: #ccc;cursor: not-allowed;transform: none;box-shadow: none;
}/* 加载动画 */
.loading .typing-indicator {display: flex;align-items: center;gap: 4px;
}.typing-indicator span {width: 8px;height: 8px;border-radius: 50%;background-color: #999;animation: typing 1.4s infinite ease-in-out;
}.typing-indicator span:nth-child(1) {animation-delay: -0.32s;
}.typing-indicator span:nth-child(2) {animation-delay: -0.16s;
}@keyframes typing {0%,80%,100% {transform: scale(0.8);opacity: 0.5;}40% {transform: scale(1);opacity: 1;}
}/* 滚动条样式 */
.chat-messages::-webkit-scrollbar {width: 6px;
}.chat-messages::-webkit-scrollbar-track {background: #f1f1f1;
}.chat-messages::-webkit-scrollbar-thumb {background: #c1c1c1;border-radius: 3px;
}.chat-messages::-webkit-scrollbar-thumb:hover {background: #a8a8a8;
}/* 消息内容样式优化 */
.message-bubble strong {font-weight: 600;color: #1976d2;
}.message-bubble .emoji {font-size: 1.2em;
}/* 响应式设计 */
@media (max-width: 768px) {.chat-dialog {height: 500px;}.message-content {max-width: 85%;}.chat-input-area {padding: 15px;}
}
</style>

http://www.dtcms.com/a/508286.html

相关文章:

  • html网站开发例子wordpress 远程附件
  • 在线心理健康网站建设杨凌做网站
  • Git将本地项目推送到GitLab
  • 廊坊网站建设公司哪个好北京微信网站设计报价
  • wordpress搜索返回页面内容优化seo公司哪家好
  • Termux 安装盘搜搜PanSou,快速找到网盘资源链接,支持各大网盘,自定义部署,数据存储到手机,打造移动搜索资源库
  • Foundation 网格实例
  • 股票300394(天孚通信)2025年4月20日
  • 公司网站怎么做啊ui设计行业的现状和发展前景
  • 专门做图片是网站深圳百度首页优化
  • 清镇网站建设推广科技感网站设计
  • GEO内容更新与迭代策略:长青内容vs时效内容的平衡
  • 专业网站优化推广医疗网站设计风格
  • 贵州毕节建设局网站官网网络营销策略包括哪些方面
  • Hugging Face 2025年10月20日 Top 10 热门AI模型
  • C#基础——GC(垃圾回收)的工作流程与优化策略
  • 空调维修技术支持深圳网站建设建设公司需要网站吗
  • 扩展-docker harbor
  • 【java面向对象进阶】------多态
  • 湖南常德广宇建设网站个人开个装修小公司
  • SSAS-如何通过Visual Studio直连SSAS
  • SAIL-VL2本地部署教程:2B/8B参数媲美大规模模型,为轻量级设备量身打造的多模态大脑
  • 卯兔科技网站建设云数据库可以做网站吗
  • wap网站建设兴田德润实惠网站开发外包合同范本
  • h5游戏免费下载:危险货车
  • 设置ubuntu系统时间为北京时间
  • TiDB和MySQL的不兼容点
  • Unity中rb.MovePosition的误区和相关物理系统知识详解
  • 基于W5500芯片实现DHCP自动获取IP功能
  • 了解学习Python3编程之面向对象