|
|
@@ -0,0 +1,1034 @@
|
|
|
+<template>
|
|
|
+ <div :class="containerClass">
|
|
|
+ <el-card class="box-card" v-if="!isFloating">
|
|
|
+ <div slot="header" class="clearfix">
|
|
|
+ <span>AI 智能问答</span>
|
|
|
+ </div>
|
|
|
+ <div class="chat-container">
|
|
|
+ <!-- 聊天记录区域 -->
|
|
|
+ <div class="chat-messages" ref="chatMessages">
|
|
|
+ <div v-for="(message, index) in chatHistory" :key="index" :class="['message', message.role]">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-avatar" v-if="message.role === 'user'">
|
|
|
+ <i class="el-icon-user"></i>
|
|
|
+ </div>
|
|
|
+ <div class="message-avatar ai-avatar" v-else-if="message.role === 'assistant'">
|
|
|
+ <i class="el-icon-s-opportunity"></i>
|
|
|
+ </div>
|
|
|
+ <div class="message-text" v-html="message.content">
|
|
|
+ </div>
|
|
|
+ <div class="message-completed" v-if="message.role === 'assistant' && message.completed">
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 建议问题区域 -->
|
|
|
+ <div class="suggested-questions" v-if="message.role === 'assistant' && message.completed && message.suggestions && message.suggestions.length > 0">
|
|
|
+ <div class="suggestion-title">您可能想问:</div>
|
|
|
+ <div class="suggestion-list">
|
|
|
+ <div v-for="(suggestion, idx) in message.suggestions" :key="idx"
|
|
|
+ class="suggestion-item" @click="handleSuggestionClick(suggestion)">
|
|
|
+ {{ suggestion }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- AI思考中提示 -->
|
|
|
+ <div class="message system thinking-message" v-if="isThinking">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="thinking-indicator">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <span v-html="aiThought"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 打字机效果区域 -->
|
|
|
+ <div class="message ai" v-if="isStreaming">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-avatar ai-avatar">
|
|
|
+ <i class="el-icon-s-opportunity"></i>
|
|
|
+ </div>
|
|
|
+ <div class="message-text">
|
|
|
+ <div v-html="currentStreamingText"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 加载中提示 -->
|
|
|
+ <div class="loading-indicator" v-if="isLoading && !isStreaming">
|
|
|
+ <div class="loading-content">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <span>正在思考中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 输入区域 -->
|
|
|
+ <div class="chat-input">
|
|
|
+ <el-input
|
|
|
+ type="textarea"
|
|
|
+ :rows="3"
|
|
|
+ placeholder="请输入您的问题..."
|
|
|
+ v-model="userInput"
|
|
|
+ :disabled="isLoading || isStreaming"
|
|
|
+ @keyup.ctrl.enter.native="sendMessage"
|
|
|
+ ></el-input>
|
|
|
+ <div class="button-container">
|
|
|
+ <el-button type="primary" :disabled="isLoading || isStreaming || !userInput.trim()" @click="sendMessage">发送</el-button>
|
|
|
+ <el-button v-if="isStreaming" @click="stopStreaming">停止</el-button>
|
|
|
+ <el-button @click="clearChat">清空对话</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 悬浮模式 -->
|
|
|
+ <div class="chat-container floating-chat-container" v-else>
|
|
|
+ <!-- 聊天记录区域 -->
|
|
|
+ <div class="chat-messages floating-chat-messages" ref="chatMessages">
|
|
|
+ <div v-for="(message, index) in chatHistory" :key="index" :class="['message', message.role]">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-avatar" v-if="message.role === 'user'">
|
|
|
+ <i class="el-icon-user"></i>
|
|
|
+ </div>
|
|
|
+ <div class="message-avatar ai-avatar" v-else-if="message.role === 'assistant'">
|
|
|
+ <!-- <i class="el-icon-s-opportunity"></i> -->
|
|
|
+ <img src="@/assets/icons/ai.png" alt="Custom Icon" class="custom-icon">
|
|
|
+ </div>
|
|
|
+ <div class="message-text" v-html="message.content">
|
|
|
+ </div>
|
|
|
+ <div class="message-completed" v-if="message.role === 'assistant' && message.completed">
|
|
|
+ <i class="el-icon-check"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 建议问题区域 -->
|
|
|
+ <div class="suggested-questions" v-if="message.role === 'assistant' && message.completed && message.suggestions && message.suggestions.length > 0">
|
|
|
+ <div class="suggestion-title">您可能想问:</div>
|
|
|
+ <div class="suggestion-list">
|
|
|
+ <div v-for="(suggestion, idx) in message.suggestions" :key="idx"
|
|
|
+ class="suggestion-item" @click="handleSuggestionClick(suggestion)">
|
|
|
+ {{ suggestion }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- AI思考中提示 -->
|
|
|
+ <!-- <div class="message system thinking-message" v-if="isThinking">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="thinking-indicator">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <span v-html="aiThought"></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div> -->
|
|
|
+
|
|
|
+ <!-- 打字机效果区域 -->
|
|
|
+ <div class="message ai" v-if="isStreaming">
|
|
|
+ <div class="message-content">
|
|
|
+ <div class="message-avatar ai-avatar">
|
|
|
+ <img src="@/assets/icons/ai.png" alt="Custom Icon" class="custom-icon">
|
|
|
+ </div>
|
|
|
+ <div class="message-text">
|
|
|
+ <div v-html="currentStreamingText"></div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 加载中提示 -->
|
|
|
+ <div class="loading-indicator" v-if="isStreaming">
|
|
|
+ <div class="loading-content">
|
|
|
+ <i class="el-icon-loading"></i>
|
|
|
+ <span>正在思考中...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 输入区域 -->
|
|
|
+ <div class="chat-input floating-chat-input">
|
|
|
+ <el-input
|
|
|
+ type="textarea"
|
|
|
+ :rows="2"
|
|
|
+ placeholder="请输入您的问题..."
|
|
|
+ v-model="userInput"
|
|
|
+ :disabled="isLoading || isStreaming"
|
|
|
+ @keyup.ctrl.enter.native="sendMessage"
|
|
|
+ ></el-input>
|
|
|
+ <div class="button-container">
|
|
|
+ <el-button type="primary" :disabled="isLoading || isStreaming || !userInput.trim()" @click="sendMessage">发送</el-button>
|
|
|
+ <el-button v-if="isStreaming && currentStreamingText" @click="stopStreaming">停止</el-button>
|
|
|
+ <el-button @click="clearChat">清空</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { chatStreamSuggested, postChatMessageData, stopChatStream } from "@/api/ai/chat";
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "AiChat",
|
|
|
+ props: {
|
|
|
+ isFloating: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ userInput: "",
|
|
|
+ chatHistory: [],
|
|
|
+ isLoading: false,
|
|
|
+ isStreaming: false,
|
|
|
+ currentStreamingText: "",
|
|
|
+ eventSource: null,
|
|
|
+ messageQueue: [],
|
|
|
+ sessionId: null,
|
|
|
+ isThinking: false, // 新增思考中状态
|
|
|
+ aiThought: "", // 新增思考内容
|
|
|
+ taskId: null, // 任务ID,用于停止响应
|
|
|
+ messageId: null, // 消息ID,用于标识当前消息
|
|
|
+ isProcessingQueue: false, // 标记是否正在处理消息队列
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ containerClass() {
|
|
|
+ return {
|
|
|
+ 'app-container': !this.isFloating,
|
|
|
+ 'floating-container': this.isFloating
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.scrollToBottom();
|
|
|
+ },
|
|
|
+ updated() {
|
|
|
+ this.scrollToBottom();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ sendMessage() {
|
|
|
+ if (!this.userInput.trim()) return;
|
|
|
+
|
|
|
+ // 添加用户消息到聊天记录
|
|
|
+ const userMessage = {
|
|
|
+ role: "user",
|
|
|
+ content: this.userInput.replace(/\n/g, "<br>")
|
|
|
+ };
|
|
|
+ this.chatHistory.push(userMessage);
|
|
|
+
|
|
|
+ // 清空输入框
|
|
|
+ const question = this.userInput;
|
|
|
+ this.userInput = "";
|
|
|
+
|
|
|
+ // 设置加载状态
|
|
|
+ this.isLoading = true;
|
|
|
+ this.isThinking = false; // 确保思考状态为false
|
|
|
+ this.aiThought = ""; // 清空思考内容
|
|
|
+
|
|
|
+ // 显示正在思考的提示
|
|
|
+ // this.$message({
|
|
|
+ // message: '正在思考您的问题...',
|
|
|
+ // type: 'info',
|
|
|
+ // duration: 2000
|
|
|
+ // });
|
|
|
+
|
|
|
+ // 开始接收流式响应
|
|
|
+ this.startStreaming(question);
|
|
|
+ },
|
|
|
+
|
|
|
+ startStreaming(question) {
|
|
|
+ this.isLoading = false;
|
|
|
+ this.isStreaming = true;
|
|
|
+ this.currentStreamingText = "";
|
|
|
+ this.isThinking = false; // 确保思考状态为false
|
|
|
+ this.aiThought = ""; // 清空思考内容
|
|
|
+
|
|
|
+ // 关闭之前的连接(如果有)
|
|
|
+ if (this.eventSource) {
|
|
|
+ this.eventSource.close();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成一个随机的会话ID,用于停止流
|
|
|
+ this.sessionId = Date.now().toString();
|
|
|
+
|
|
|
+ // 创建消息对象
|
|
|
+ const messageData = {
|
|
|
+ query: question,
|
|
|
+ user: this.sessionId
|
|
|
+ };
|
|
|
+ console.log("this.sessionId:开始",this.sessionId);
|
|
|
+ // 使用fetch发送POST请求并处理流式响应
|
|
|
+ postChatMessageData(messageData).then(response => {
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP错误 ${response.status} ${response.statusText}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取响应流的读取器
|
|
|
+ const reader = response.body.getReader();
|
|
|
+ const decoder = new TextDecoder('utf-8');
|
|
|
+ let buffer = '';
|
|
|
+
|
|
|
+ // 读取流数据
|
|
|
+ const readStream = () => {
|
|
|
+ if (!this.isStreaming) {
|
|
|
+ console.log("流已停止");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ reader.read().then(({ done, value }) => {
|
|
|
+ if (done) {
|
|
|
+ console.log("流读取完成");
|
|
|
+ this.finishStreaming();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 解码接收到的数据块
|
|
|
+ buffer += decoder.decode(value, { stream: true });
|
|
|
+
|
|
|
+ // 处理SSE格式的数据(按行拆分)
|
|
|
+ const lines = buffer.split('\n');
|
|
|
+ buffer = lines.pop() || ''; // 最后一行可能不完整,保存到buffer
|
|
|
+
|
|
|
+ // 变量来跟踪当前事件类型和数据
|
|
|
+ let currentEvent = 'agent_message'; // 默认为agent_message事件类型
|
|
|
+ let currentData = '';
|
|
|
+
|
|
|
+ for (let i = 0; i < lines.length; i++) {
|
|
|
+ const line = lines[i].trim();
|
|
|
+
|
|
|
+ // 空行表示一个事件的结束
|
|
|
+ if (line === '') {
|
|
|
+ if (currentData) {
|
|
|
+ // 如果有数据,处理当前事件
|
|
|
+ this.handleEvent(currentEvent, currentData);
|
|
|
+ currentData = '';
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 事件类型行
|
|
|
+ if (line.startsWith('event:')) {
|
|
|
+ currentEvent = line.slice(6).trim();
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 数据行
|
|
|
+ if (line.startsWith('data:')) {
|
|
|
+ const dataContent = line.slice(5).trim();
|
|
|
+ if (dataContent) {
|
|
|
+ // 初始化或追加数据
|
|
|
+ if (currentData) {
|
|
|
+ currentData += '\n' + dataContent;
|
|
|
+ } else {
|
|
|
+ currentData = dataContent;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理缓冲区中可能剩余的完整事件
|
|
|
+ if (currentData) {
|
|
|
+ this.handleEvent(currentEvent, currentData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 继续读取流
|
|
|
+ readStream();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("数据解析错误:", error);
|
|
|
+ this.handleError("数据解析错误: " + (error.message || "未知错误"));
|
|
|
+ this.finishStreaming();
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ console.error("Stream reading error:", error);
|
|
|
+ this.handleError("读取数据流错误: " + (error.message || "未知错误"));
|
|
|
+ this.finishStreaming();
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 开始读取流
|
|
|
+ readStream();
|
|
|
+
|
|
|
+ }).catch(error => {
|
|
|
+ console.error("Fetch error:", error);
|
|
|
+ this.handleError("请求错误: " + (error.message || "未知错误"));
|
|
|
+ this.finishStreaming();
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ processMessageQueue() {
|
|
|
+ this.isProcessingQueue = true;
|
|
|
+
|
|
|
+ const processNextChunk = () => {
|
|
|
+ if (this.messageQueue.length > 0) {
|
|
|
+ // 每次只处理一小部分文本,以实现打字机效果
|
|
|
+ const chunk = this.messageQueue.shift();
|
|
|
+
|
|
|
+ // 处理内容 - 不需要类型检查,直接添加内容到当前流式文本
|
|
|
+ this.currentStreamingText += chunk;
|
|
|
+
|
|
|
+ // 滚动到底部
|
|
|
+ this.scrollToBottom();
|
|
|
+
|
|
|
+ // 使用setTimeout延迟处理下一个块,创造打字机效果
|
|
|
+ setTimeout(() => {
|
|
|
+ processNextChunk();
|
|
|
+ }, 10); // 调整延迟时间可以控制打字速度
|
|
|
+ } else {
|
|
|
+ this.isProcessingQueue = false;
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.messageQueue.length > 0) {
|
|
|
+ this.processMessageQueue();
|
|
|
+ }
|
|
|
+ }, 20);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ processNextChunk();
|
|
|
+ },
|
|
|
+
|
|
|
+ finishStreaming() {
|
|
|
+ // 设置流结束标志,流读取器会检查这个标志
|
|
|
+ this.isStreaming = false;
|
|
|
+ this.isThinking = false;
|
|
|
+
|
|
|
+ // 等待所有消息处理完毕
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.currentStreamingText) {
|
|
|
+ // 创建消息对象
|
|
|
+ const assistantMessage = {
|
|
|
+ role: "assistant",
|
|
|
+ content: this.currentStreamingText,
|
|
|
+ completed: true, // 添加完成标记
|
|
|
+ suggestions: [] // 初始化建议问题数组
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加到聊天记录
|
|
|
+ this.chatHistory.push(assistantMessage);
|
|
|
+
|
|
|
+ // 获取建议问题
|
|
|
+ this.fetchSuggestedQuestions(assistantMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置状态
|
|
|
+ this.currentStreamingText = "";
|
|
|
+ this.isLoading = false;
|
|
|
+ this.messageQueue = [];
|
|
|
+ this.aiThought = "";
|
|
|
+ console.log("this.sessionId:结束",this.sessionId);
|
|
|
+
|
|
|
+ // 最终滚动到底部确保内容可见
|
|
|
+ this.scrollToBottom();
|
|
|
+ }, 200); // 短暂延迟确保所有内容已处理完毕
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取建议问题
|
|
|
+ fetchSuggestedQuestions(assistantMessage) {
|
|
|
+ // 调用API获取建议问题
|
|
|
+ chatStreamSuggested({user: this.sessionId,messageId:this.messageId}).then(response => {
|
|
|
+ if (response && response.data) {
|
|
|
+ // 检查API响应格式,可能直接是数组或者包装在data属性中
|
|
|
+ let suggestions = Array.isArray(response.data) ? response.data :
|
|
|
+ (Array.isArray(response.data.suggestions) ? response.data.suggestions : []);
|
|
|
+
|
|
|
+ // 最多显示5个建议问题
|
|
|
+ assistantMessage.suggestions = suggestions.slice(0, 5);
|
|
|
+
|
|
|
+ // 强制更新视图
|
|
|
+ this.$forceUpdate();
|
|
|
+ // 确保滚动到底部显示建议问题
|
|
|
+ this.scrollToBottom();
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ console.error("获取建议问题失败:", error);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ stopStreaming() {
|
|
|
+ // 设置标志位停止流读取
|
|
|
+ this.isStreaming = false;
|
|
|
+ this.isThinking = false;
|
|
|
+
|
|
|
+ // 调用API终止流
|
|
|
+ if (this.sessionId) {
|
|
|
+ stopChatStream({user:this.sessionId,taskId:this.taskId}).catch(error => {
|
|
|
+ console.error("Error stopping stream:", error);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 完成当前的流式响应
|
|
|
+ if (this.currentStreamingText) {
|
|
|
+ // 创建中断消息对象
|
|
|
+ const interruptedMessage = {
|
|
|
+ role: "assistant",
|
|
|
+ content: this.currentStreamingText + " [用户中断]",
|
|
|
+ completed: true,
|
|
|
+ suggestions: []
|
|
|
+ };
|
|
|
+
|
|
|
+ // 将流式文本添加到聊天记录,标记为用户中断
|
|
|
+ this.chatHistory.push(interruptedMessage);
|
|
|
+
|
|
|
+ // 尝试获取建议问题
|
|
|
+ this.fetchSuggestedQuestions(interruptedMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重置状态
|
|
|
+ this.currentStreamingText = "";
|
|
|
+ this.isLoading = false;
|
|
|
+ this.messageQueue = [];
|
|
|
+ this.aiThought = "";
|
|
|
+ },
|
|
|
+
|
|
|
+ clearChat() {
|
|
|
+ this.chatHistory = [];
|
|
|
+ this.stopStreaming();
|
|
|
+ this.isThinking = false;
|
|
|
+ this.aiThought = "";
|
|
|
+ },
|
|
|
+
|
|
|
+ scrollToBottom() {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.$refs.chatMessages) {
|
|
|
+ const container = this.$refs.chatMessages;
|
|
|
+ container.scrollTop = container.scrollHeight;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理SSE事件
|
|
|
+ handleEvent(eventName, eventData) {
|
|
|
+ // console.log(`收到事件 ${eventName}:`, eventData);
|
|
|
+
|
|
|
+ // 处理ping事件,不尝试解析为JSON
|
|
|
+ if (eventName === 'ping') {
|
|
|
+ console.log("收到ping事件");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 尝试解析JSON数据
|
|
|
+ const data = JSON.parse(eventData);
|
|
|
+
|
|
|
+ switch (eventName) {
|
|
|
+ case 'agent_message':
|
|
|
+ // 处理agent_message事件
|
|
|
+ if (data && data.answer !== undefined) {
|
|
|
+ // 添加回答内容到消息队列
|
|
|
+ this.messageId = data.id; // 保存消息ID
|
|
|
+ this.taskId = data.task_id; // 保存任务ID用于停止响应
|
|
|
+ const answer = data.answer.toString();
|
|
|
+ if (answer.trim()) {
|
|
|
+ this.isThinking = false; // 收到回答,关闭思考状态
|
|
|
+ this.messageQueue.push(answer);
|
|
|
+
|
|
|
+ if (!this.isProcessingQueue) {
|
|
|
+ this.processMessageQueue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'message':
|
|
|
+ // 处理message事件
|
|
|
+ if (data && data.answer !== undefined) {
|
|
|
+ const answer = data.answer.toString();
|
|
|
+ if (answer.trim()) {
|
|
|
+ this.isThinking = false; // 收到回答,关闭思考状态
|
|
|
+ this.messageQueue.push(answer);
|
|
|
+
|
|
|
+ if (!this.isProcessingQueue) {
|
|
|
+ this.processMessageQueue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (data && data.eventType === 'MESSAGE_END') {
|
|
|
+ console.log("Message end event received");
|
|
|
+ this.finishStreaming();
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'message_end':
|
|
|
+ // 消息结束事件,完成流式处理
|
|
|
+ // console.log("Message end event received:", data);
|
|
|
+ this.finishStreaming();
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'agent_thought':
|
|
|
+ // 处理思考过程
|
|
|
+ if (data && data.thought && data.thought.trim() !== '') {
|
|
|
+ // console.log("AI思考:", data.thought);
|
|
|
+
|
|
|
+ // 格式化思考内容,添加前缀
|
|
|
+ // const formattedThought = `<strong>AI思考:</strong><br>${data.thought.replace(/\n/g, '<br>')}`;
|
|
|
+
|
|
|
+ // 更新思考内容并显示思考状态
|
|
|
+ // this.aiThought = formattedThought;
|
|
|
+ this.isThinking = true;
|
|
|
+
|
|
|
+ // 确保滚动到可见区域
|
|
|
+ // this.$nextTick(() => {
|
|
|
+ // const thinkingElement = document.querySelector('.thinking-message');
|
|
|
+ // if (thinkingElement) {
|
|
|
+ // thinkingElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
|
|
+ // }
|
|
|
+ // });
|
|
|
+ }
|
|
|
+ break;
|
|
|
+
|
|
|
+ case 'error':
|
|
|
+ // 处理错误事件
|
|
|
+ console.error("Error event:", data);
|
|
|
+ this.isThinking = false;
|
|
|
+ if (data && data.message) {
|
|
|
+ this.$message.error(`错误: ${data.message}`);
|
|
|
+ } else {
|
|
|
+ this.$message.error("发生未知错误");
|
|
|
+ }
|
|
|
+ this.finishStreaming();
|
|
|
+ break;
|
|
|
+
|
|
|
+ default:
|
|
|
+ // 其他事件类型,尝试提取answer字段
|
|
|
+ if (data && data.answer !== undefined) {
|
|
|
+ const answer = data.answer.toString();
|
|
|
+ if (answer.trim()) {
|
|
|
+ this.isThinking = false; // 收到回答,关闭思考状态
|
|
|
+ this.messageQueue.push(answer);
|
|
|
+
|
|
|
+ if (!this.isProcessingQueue) {
|
|
|
+ this.processMessageQueue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ // 如果解析JSON失败,尝试直接使用原始数据
|
|
|
+ console.error("Error parsing event data:", error);
|
|
|
+ if (eventName === 'message_end') {
|
|
|
+ this.finishStreaming();
|
|
|
+ } else if (eventName === 'agent_thought') {
|
|
|
+ // 尝试处理可能的非JSON格式的思考内容
|
|
|
+ if (typeof eventData === 'string' && eventData.trim()) {
|
|
|
+ this.aiThought = eventData;
|
|
|
+ this.isThinking = true;
|
|
|
+ }
|
|
|
+ } else if (eventName !== 'error') {
|
|
|
+ // 非控制类事件,直接添加到消息队列
|
|
|
+ if (typeof eventData === 'string' && eventData.trim()) {
|
|
|
+ this.isThinking = false; // 收到回答,关闭思考状态
|
|
|
+ // 将原始文本添加到队列
|
|
|
+ this.messageQueue.push(eventData);
|
|
|
+
|
|
|
+ if (!this.isProcessingQueue) {
|
|
|
+ this.processMessageQueue();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 统一处理错误
|
|
|
+ handleError(errorMsg) {
|
|
|
+ this.$message.error(errorMsg);
|
|
|
+
|
|
|
+ // 将错误信息显示在聊天窗口
|
|
|
+ if (this.currentStreamingText) {
|
|
|
+ this.chatHistory.push({
|
|
|
+ role: "assistant",
|
|
|
+ content: this.currentStreamingText
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用系统消息添加错误信息
|
|
|
+ this.addSystemMessage(`<span style="color: red;"><i class="el-icon-warning"></i> ${errorMsg}</span>`);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加系统消息到聊天记录
|
|
|
+ addSystemMessage(content) {
|
|
|
+ this.chatHistory.push({
|
|
|
+ role: "system",
|
|
|
+ content: content
|
|
|
+ });
|
|
|
+ this.scrollToBottom();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理建议问题点击
|
|
|
+ handleSuggestionClick(suggestion) {
|
|
|
+ this.userInput = suggestion;
|
|
|
+ // this.sendMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.chat-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 70vh;
|
|
|
+}
|
|
|
+
|
|
|
+.chat-messages {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ background-image: url('~@/assets/images/chat-bg-pattern.png');
|
|
|
+ background-repeat: repeat;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.message {
|
|
|
+ margin-bottom: 15px;
|
|
|
+ animation: fadeIn 0.5s ease;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes fadeIn {
|
|
|
+ from { opacity: 0; transform: translateY(10px); }
|
|
|
+ to { opacity: 1; transform: translateY(0); }
|
|
|
+}
|
|
|
+
|
|
|
+.message-content {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+
|
|
|
+.message-avatar {
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background-color: #409eff;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: white;
|
|
|
+ margin-right: 10px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.ai-avatar {
|
|
|
+ background-color: #67c23a;
|
|
|
+}
|
|
|
+
|
|
|
+.message-text {
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ max-width: calc(100% - 60px);
|
|
|
+ word-break: break-word;
|
|
|
+ white-space: pre-wrap; /* 保留换行和空格 */
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.user .message-text {
|
|
|
+ background-color: #ecf5ff;
|
|
|
+ border: 1px solid #d9ecff;
|
|
|
+}
|
|
|
+
|
|
|
+.ai .message-text {
|
|
|
+ background-color: #f0f9eb;
|
|
|
+ border: 1px solid #e1f3d8;
|
|
|
+ background-image: linear-gradient(to bottom right, #f0f9eb, #e8f5e9);
|
|
|
+}
|
|
|
+
|
|
|
+.message-completed {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ margin-left: 5px;
|
|
|
+ color: #67c23a;
|
|
|
+ font-size: 14px;
|
|
|
+ opacity: 0;
|
|
|
+ animation: fadeIn 0.5s ease forwards 0.5s;
|
|
|
+}
|
|
|
+
|
|
|
+.message-completed i {
|
|
|
+ background-color: #f0f9eb;
|
|
|
+ border-radius: 50%;
|
|
|
+ padding: 3px;
|
|
|
+ border: 1px solid #e1f3d8;
|
|
|
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.system .message-content {
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.system .message-text {
|
|
|
+ background-color: #fef0f0;
|
|
|
+ border: 1px solid #fde2e2;
|
|
|
+ text-align: center;
|
|
|
+ max-width: 80%;
|
|
|
+}
|
|
|
+
|
|
|
+.thought {
|
|
|
+ font-style: italic;
|
|
|
+ color: #606266;
|
|
|
+ background-color: #f4f4f5;
|
|
|
+ border: 1px solid #e9e9eb;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 5px 10px;
|
|
|
+ display: block;
|
|
|
+ margin: 5px 0;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes blink {
|
|
|
+ 0%, 100% { opacity: 1; }
|
|
|
+ 50% { opacity: 0; }
|
|
|
+}
|
|
|
+
|
|
|
+.chat-input {
|
|
|
+ margin-top: 10px;
|
|
|
+ background-color: #fff;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.button-container {
|
|
|
+ margin-top: 10px;
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-indicator {
|
|
|
+ text-align: center;
|
|
|
+ padding: 10px;
|
|
|
+ color: #909399;
|
|
|
+ margin: 10px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-content {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ background-color: #f4f4f5;
|
|
|
+ padding: 8px 16px;
|
|
|
+ border-radius: 16px;
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.loading-content i {
|
|
|
+ color: #409eff;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ from { transform: rotate(0deg); }
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+/* 思考中提示样式 */
|
|
|
+.thinking-message {
|
|
|
+ margin-top: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.thinking-message .message-content {
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.thinking-indicator {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ color: #67c23a;
|
|
|
+ background-color: #f0f9eb;
|
|
|
+ border: 1px solid #e1f3d8;
|
|
|
+ padding: 8px 15px;
|
|
|
+ border-radius: 16px;
|
|
|
+ font-size: 14px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
|
+ animation: pulseThinking 2s infinite;
|
|
|
+ max-width: 80%;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulseThinking {
|
|
|
+ 0% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0.4); }
|
|
|
+ 70% { box-shadow: 0 0 0 10px rgba(103, 194, 58, 0); }
|
|
|
+ 100% { box-shadow: 0 0 0 0 rgba(103, 194, 58, 0); }
|
|
|
+}
|
|
|
+
|
|
|
+.thinking-indicator i {
|
|
|
+ color: #67c23a;
|
|
|
+ font-size: 16px;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加对思考内容的样式增强 */
|
|
|
+.thinking-indicator span {
|
|
|
+ flex: 1;
|
|
|
+ word-break: break-word;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ line-height: 1.4;
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+ text-align: left;
|
|
|
+ padding: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 悬浮窗样式 */
|
|
|
+.floating-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.floating-chat-container {
|
|
|
+ height: 100%;
|
|
|
+ padding: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background-color: transparent;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+.floating-chat-messages {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f9f9f9;
|
|
|
+ background-image: url('~@/assets/images/chat-bg-pattern.png');
|
|
|
+ background-repeat: repeat;
|
|
|
+ border-radius: 0;
|
|
|
+ margin-bottom: 0;
|
|
|
+ box-shadow: none;
|
|
|
+}
|
|
|
+
|
|
|
+.floating-chat-input {
|
|
|
+ padding: 8px;
|
|
|
+ background-color: #fff;
|
|
|
+ border-top: 1px solid #e6e6e6;
|
|
|
+ margin-top: 0;
|
|
|
+ box-shadow: none;
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.floating-chat-input .el-textarea__inner {
|
|
|
+ min-height: 60px !important;
|
|
|
+ max-height: 80px !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 悬浮模式下建议问题样式调整 */
|
|
|
+.floating-chat-messages .suggested-questions {
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.floating-chat-messages .suggestion-list {
|
|
|
+ gap: 6px;
|
|
|
+ padding-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.floating-chat-messages .suggestion-item {
|
|
|
+ padding: 4px 8px;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .floating-chat-container {
|
|
|
+ height: calc(100% - 10px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .floating-chat-messages {
|
|
|
+ max-height: calc(100vh - 170px);
|
|
|
+ }
|
|
|
+
|
|
|
+ .floating-chat-input {
|
|
|
+ padding: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .floating-chat-input .button-container {
|
|
|
+ padding: 5px 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ .floating-chat-input .el-button {
|
|
|
+ padding: 7px 12px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 移动端下的建议问题样式 */
|
|
|
+ .suggested-questions {
|
|
|
+ padding: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .suggestion-title {
|
|
|
+ margin-bottom: 5px;
|
|
|
+ font-size: 13px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .suggestion-list {
|
|
|
+ gap: 5px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .suggestion-item {
|
|
|
+ padding: 4px 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* 建议问题样式 */
|
|
|
+.suggested-questions {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #e6f7ff;
|
|
|
+ border: 1px solid #91d5ff;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.suggestion-title {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #1890ff;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ padding-left: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.suggestion-list {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+ padding-left: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.suggestion-item {
|
|
|
+ background-color: #fff;
|
|
|
+ border: 1px solid #d9d9d9;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 5px 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #333;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.2s ease;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+
|
|
|
+.suggestion-item:hover {
|
|
|
+ background-color: #e6f7ff;
|
|
|
+ border-color: #91d5ff;
|
|
|
+}
|
|
|
+
|
|
|
+.suggestion-item:active {
|
|
|
+ background-color: #bae7ff;
|
|
|
+}
|
|
|
+
|
|
|
+.custom-icon {
|
|
|
+ width: 34px; /* 设置图标宽度 */
|
|
|
+ height: 34px; /* 设置图标高度 */
|
|
|
+ vertical-align: middle; /* 垂直对齐 */
|
|
|
+}
|
|
|
+</style>
|