zhuliu 7 月之前
父节点
当前提交
5589658d2b
共有 6 个文件被更改,包括 553 次插入2 次删除
  1. 1 0
      public/index.html
  2. 4 1
      src/api/index.js
  3. 7 0
      src/api/newApi/AI.js
  4. 536 0
      src/utils/model/AIChat/AIChatBox.vue
  5. 4 1
      src/utils/model/index.js
  6. 1 0
      src/views/home/index.vue

+ 1 - 0
public/index.html

@@ -5,6 +5,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
     <link rel="icon" href="<%= BASE_URL %>favicon.ico">
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
     <title><%= htmlWebpackPlugin.options.title %></title>
     <title><%= htmlWebpackPlugin.options.title %></title>
 </head>
 </head>
 <body>
 <body>

+ 4 - 1
src/api/index.js

@@ -23,6 +23,8 @@ import translate from "./newApi/translate";
 import noveltySearch from "./newApi/noveltySearch";
 import noveltySearch from "./newApi/noveltySearch";
 import IPREmail from "./newApi/IPREmail";
 import IPREmail from "./newApi/IPREmail";
 
 
+import AI from './newApi/AI'
+
 export default {
 export default {
   ...client,
   ...client,
   ...user,
   ...user,
@@ -44,5 +46,6 @@ export default {
   ...otherPatentInformation,
   ...otherPatentInformation,
   ...translate,
   ...translate,
   ...noveltySearch,
   ...noveltySearch,
-  ...IPREmail
+  ...IPREmail,
+  ...AI
 }
 }

+ 7 - 0
src/api/newApi/AI.js

@@ -0,0 +1,7 @@
+import axios from "@/utils/axios";
+
+export default {
+  AIChat(data){
+    return axios.post('/AI/chat', data)
+  },
+};

+ 536 - 0
src/utils/model/AIChat/AIChatBox.vue

@@ -0,0 +1,536 @@
+<template>
+  <div class="chat-container">
+    <div class="chat-messages" ref="messageBox">
+      <div v-for="(message, index) in chatHistory" 
+           :key="index" 
+           :class="['message', message.type]">
+        <div class="message-content">
+          <div v-if="isLoading && message === currentStreamingMessage && message.type === 'ai'"  class="message-loading">
+            <i class="el-icon-loading"></i>
+          </div>
+          <div v-if="message.type === 'ai'" class="message-actions">
+            <button class="action-btn" @click="copyMessage(message.content)" title="复制">
+              <i class="fas fa-copy"></i>
+            </button>
+            <button v-if="message === currentStreamingMessage" 
+                    class="action-btn" 
+                    @click="stopGeneration" 
+                    title="停止生成">
+              <i class="fas fa-stop"></i>
+            </button>
+            <button class="action-btn" 
+                    @click="regenerateResponse(index)" 
+                    title="重新生成"
+                    :disabled="isLoading">
+              <i class="fas fa-redo"></i>
+            </button>
+          </div>
+          <div class="content-text">{{ message.isTyping ? message.displayContent : message.content }}</div>
+          <div class="message-time">{{ message.timestamp }}</div>
+        </div>
+      </div>
+    </div>
+    
+    <div class="input-area">
+      <!-- <div class="typing-status" v-if="isLoading">AI正在思考中...</div> -->
+      <select v-model="selectedQuestion" class="preset-questions">
+        <option value="">选择预设问题...</option>
+        <option v-for="(q, index) in presetQuestions" 
+                :key="index" 
+                :value="q.question">
+          {{ q.question }}
+        </option>
+      </select>
+      
+      <div class="input-group">
+        <input type="text" 
+               v-model="userInput" 
+               @keyup.enter="sendMessage"
+               placeholder="输入您的问题...">
+        <button @click="sendMessage" :disabled="isLoading">发送</button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  name: 'AIChatBox',
+  data() {
+    return {
+      userInput: '',
+      selectedQuestion: '',
+      chatHistory: [],
+      presetQuestions: [
+        {
+          question: "你好,请问你是谁?",
+        },
+        {
+          question: "现在几点了?",
+        },
+        {
+          question: "你能做什么?",
+        },
+        {
+          question: "今天天气怎么样?",
+        }
+      ],
+      isLoading: false,
+      currentStreamingMessage: null,
+      controller: null, // 用于中止请求
+      typingSpeed: 20, // 打字机效果的速度(毫秒)
+      scrollLock:true,
+    }
+  },
+  
+  created() {
+    // 从 localStorage 加载历史记录
+    const savedHistory = localStorage.getItem('chatHistory')
+    if (savedHistory) {
+      this.chatHistory = JSON.parse(savedHistory)
+    }
+  },
+  
+  watch: {
+    chatHistory: {
+      handler(newVal) {
+        // 保存到 localStorage
+        localStorage.setItem('chatHistory', JSON.stringify(newVal))
+      },
+      deep: true
+    },
+    selectedQuestion(val) {
+      if (val) {
+        this.userInput = val;
+      }
+    }
+  },
+  mounted() {
+    // 添加滚动事件监听
+    const messageBox = this.$refs.messageBox;
+    if (messageBox) {
+      messageBox.addEventListener('scroll', this.handleScroll);
+    }
+    
+    // 初始滚动到底部
+    this.scrollToBottom();
+  },
+
+  beforeDestroy() {
+    // 移除滚动事件监听
+    const messageBox = this.$refs.messageBox;
+    if (messageBox) {
+      messageBox.removeEventListener('scroll', this.handleScroll);
+    }
+  },
+
+  updated() {
+    // 当内容更新时滚动到底部
+    this.scrollToBottom();
+  },
+  
+  methods: {
+    //发送信息
+    async sendMessage() {
+      if (!this.userInput.trim()) return;
+      
+      const userMessage = {
+        type: 'user',
+        content: this.userInput,
+        timestamp: new Date().toLocaleTimeString()
+      };
+      
+      this.chatHistory.push(userMessage);
+      this.isLoading = true;
+      
+      const aiMessage = {
+        type: 'ai',
+        content: '',
+        displayContent: '', // 用于打字机效果
+        isTyping: true,
+        timestamp: new Date().toLocaleTimeString()
+      };
+      
+      this.chatHistory.push(aiMessage);
+      this.currentStreamingMessage = aiMessage;
+      
+      try {
+        await this.getAIResponse(this.userInput);
+      } catch (error) {
+        if (error.name === 'AbortError') {
+          this.currentStreamingMessage.content += '\n[生成已停止]';
+        } else {
+          console.error('AI响应错误:', error);
+          this.currentStreamingMessage.content = '抱歉,服务出现了问题,请稍后再试。';
+        }
+      } finally {
+        this.isLoading = false;
+        this.userInput = '';
+        this.selectedQuestion = '';
+        await this.finishTypingEffect(this.currentStreamingMessage);
+        this.currentStreamingMessage = null;
+        this.controller = null;
+        this.$nextTick(() => {
+          this.scrollToBottom();
+        });
+      }
+    },
+    //获取回复信息
+    async getAIResponse(message) {
+      this.controller = new AbortController();
+      
+      try {
+        const response = await fetch('/api/AI/chat', {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+            'Accept': 'text/event-stream',
+          },
+          body: JSON.stringify({
+            messages: [{
+              role: 'user',
+              content: message
+            }],
+            // model: 'gpt-3.5-turbo',
+            stream: true,
+          }),
+          signal: this.controller.signal
+        });
+
+        if (!response.ok) throw new Error('AI API 调用失败');
+
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder('utf-8');
+
+        while (true) {
+          const { done, value } = await reader.read();
+          if (done) break;
+
+          const chunk = decoder.decode(value);
+          const lines = chunk
+            .split('\n')
+            .filter(line => line.trim() !== '' && line.trim() !== 'data: [DONE]');
+
+          for (const line of lines) {
+            try {
+              const jsonStr = line.replace(/^data: /, '');
+              const json = JSON.parse(jsonStr);
+              
+              if (json.choices[0]?.delta?.content) {
+                const newContent = json.choices[0].delta.content;
+                this.currentStreamingMessage.content += newContent;
+                await this.typeWriter(this.currentStreamingMessage, newContent);
+              }
+            } catch (e) {
+              console.error('解析响应数据出错:', e);
+            }
+          }
+        }
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    async typeWriter(message, newContent) {
+      const currentLength = message.displayContent.length;
+      const targetLength = message.content.length;
+      
+      if (currentLength < targetLength) {
+        message.displayContent = message.content.substring(0, currentLength + newContent.length);
+        this.$nextTick(() => {
+          this.scrollToBottom();
+        });
+      }
+    },
+
+    async finishTypingEffect(message) {
+      message.displayContent = message.content;
+      message.isTyping = false;
+    },
+
+    //停止生成
+    stopGeneration() {
+      if (this.controller) {
+        this.controller.abort();
+      }
+    },
+
+    //重新生成
+    async regenerateResponse(index) {
+      const originalUserMessage = this.chatHistory[index - 1];
+      if (!originalUserMessage || originalUserMessage.type !== 'user') return;
+      
+      // 删除当前回复
+      this.chatHistory.splice(index, 1);
+      
+      // 重新发送请求
+      const aiMessage = {
+        type: 'ai',
+        content: '',
+        displayContent: '',
+        isTyping: true,
+        timestamp: new Date().toLocaleTimeString()
+      };
+      
+      this.chatHistory.push(aiMessage);
+      this.currentStreamingMessage = aiMessage;
+      this.isLoading = true;
+      
+      try {
+        await this.getAIResponse(originalUserMessage.content);
+      } catch (error) {
+        console.error('重新生成失败:', error);
+        this.currentStreamingMessage.content = '重新生成失败,请稍后再试。';
+      } finally {
+        this.isLoading = false;
+        await this.finishTypingEffect(this.currentStreamingMessage);
+        this.currentStreamingMessage = null;
+      }
+    },
+
+    //复制信息
+    async copyMessage(content) {
+      try {
+        await navigator.clipboard.writeText(content);
+        // 可以添加一个提示,表示复制成功
+        this.$message?.success?.('复制成功') || alert('复制成功');
+      } catch (err) {
+        console.error('复制失败:', err);
+        this.$message?.error?.('复制失败') || alert('复制失败');
+      }
+    },
+
+    //滚动到最底端
+    scrollToBottom() {
+      if (!this.scrollLock) return;
+      
+      this.$nextTick(() => {
+        const messageBox = this.$refs.messageBox;
+        if (messageBox) {
+          // 使用平滑滚动效果
+          messageBox.scrollTo({
+            top: messageBox.scrollHeight,
+            behavior: 'instant'
+          });
+        }
+      });
+    },
+
+    // 监听滚动事件
+    handleScroll() {
+      const messageBox = this.$refs.messageBox;
+      if (!messageBox) return;
+
+      // 计算是否在底部附近(允许 100px 的误差)
+      const isNearBottom = 
+        messageBox.scrollHeight - messageBox.scrollTop - messageBox.clientHeight < 100;
+      
+      this.scrollLock = isNearBottom;
+    },
+
+    //清空历史
+    clearHistory() {
+      if (confirm('确定要清空聊天记录吗?')) {
+        this.chatHistory = [];
+        localStorage.removeItem('chatHistory');
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.chat-container {
+  width: 100%;
+  max-width: 600px;
+  margin: 0 auto;
+  border: 1px solid #ddd;
+  border-radius: 8px;
+  overflow: hidden;
+}
+
+.chat-messages {
+  height: 400px;
+  overflow-y: auto;
+  padding: 20px;
+  background: #f5f5f5;
+  scroll-behavior: smooth; 
+}
+
+.message {
+  margin-bottom: 15px;
+  /* max-width: 80%; */
+  position:relative;
+}
+
+.message.user {
+  /* margin-left: auto; */
+  display: flex;
+  flex-direction: row-reverse;
+}
+
+.message-content {
+  padding: 10px 15px;
+  border-radius: 15px;
+  display: inline-block;
+  white-space: pre-wrap;
+  word-break: break-word;
+  position: relative;
+  padding-right: 30px; /* 为按钮留出空间 */
+  max-width: 80%;
+}
+
+.user .message-content {
+  background: #007bff;
+  color: white;
+}
+
+.ai .message-content {
+  background: white;
+  color: #333;
+}
+
+.input-area {
+  padding: 15px;
+  background: white;
+}
+
+.input-group {
+  display: flex;
+  margin-top: 10px;
+}
+
+input {
+  flex: 1;
+  padding: 8px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  margin-right: 10px;
+}
+
+button {
+  padding: 8px 20px;
+  background: #007bff;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+button:hover {
+  background: #0056b3;
+}
+
+.preset-questions {
+  width: 100%;
+  padding: 8px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+}
+
+.typing-status {
+  padding: 8px;
+  color: #666;
+  font-style: italic;
+}
+
+.message-time {
+  font-size: 0.8em;
+  color: #999;
+  margin-top: 4px;
+}
+
+/* 添加一个清空历史记录的按钮样式 */
+.clear-history {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  padding: 5px 10px;
+  background: #dc3545;
+  color: white;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.message-loading{
+  position: absolute;
+  left: -20px;
+  top: 0px;
+  width: 20px;
+  height: 20px;
+}
+
+.message-actions {
+  position: absolute;
+  /* top: 5px; */
+  top: calc(100% + 5px);
+  /* right: 5px; */
+  display: flex;
+  gap: 5px;
+  opacity: 1;
+  transition: opacity 0.2s;
+}
+
+.message:hover .message-actions {
+  /* opacity: 1; */
+}
+
+.action-btn {
+  padding: 4px 8px;
+  background: rgba(255, 255, 255, 0.9);
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 12px;
+  color: #666;
+  transition: all 0.2s;
+}
+
+.action-btn:hover {
+  background: #f0f0f0;
+  color: #333;
+}
+
+.action-btn:disabled {
+  opacity: 0.5;
+  cursor: not-allowed;
+}
+
+.content-text {
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+
+/* 添加闪烁的光标效果 */
+.message.ai .content-text[data-typing="true"]::after {
+  content: '|';
+  animation: blink 1s infinite;
+}
+
+@keyframes blink {
+  0%, 100% { opacity: 1; }
+  50% { opacity: 0; }
+}
+
+/* 自定义滚动条样式 */
+.chat-messages::-webkit-scrollbar {
+  width: 8px;
+}
+
+.chat-messages::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 4px;
+}
+
+.chat-messages::-webkit-scrollbar-thumb {
+  background: #888;
+  border-radius: 4px;
+}
+
+.chat-messages::-webkit-scrollbar-thumb:hover {
+  background: #555;
+}
+</style> 

+ 4 - 1
src/utils/model/index.js

@@ -26,6 +26,8 @@ import myTabsItem from './tabs/tabsItem'
 //查新检索发明点弹窗
 //查新检索发明点弹窗
 import inventionPointDialog from '@/views/noveltySearch/components/dialog/inventionPoint/inventionPoint.vue'
 import inventionPointDialog from '@/views/noveltySearch/components/dialog/inventionPoint/inventionPoint.vue'
 
 
+import AIChatBox from './AIChat/AIChatBox.vue';
+
 var models = {
 var models = {
   myCustomSvg,
   myCustomSvg,
   myTree,
   myTree,
@@ -50,7 +52,8 @@ var models = {
   //tab
   //tab
   myTabs,
   myTabs,
   myTabsItem,
   myTabsItem,
-  inventionPointDialog
+  inventionPointDialog,
+  AIChatBox
 }
 }
 export default {
 export default {
   install(Vue) {
   install(Vue) {

+ 1 - 0
src/views/home/index.vue

@@ -1,6 +1,7 @@
 <template>
 <template>
   <div class="home">
   <div class="home">
     <div class="main">
     <div class="main">
+      <AIChatBox></AIChatBox>
       <div class="carousel" id="step1">
       <div class="carousel" id="step1">
         <carousel></carousel>
         <carousel></carousel>
       </div>
       </div>