소스 검색

靶向提升增加ai机器人抽屉弹窗 ai解读调用功能

dengshaobo 4 일 전
부모
커밋
3c79dbc632

+ 144 - 0
package-lock.json

@@ -30,6 +30,7 @@
         "jspdf": "^2.5.2",
         "kde": "^0.0.1",
         "lottie-web": "^5.12.2",
+        "markdown-it": "^15.0.0",
         "nprogress": "^0.2.0",
         "particles.js": "^2.0.0",
         "plotly.js": "^2.35.3",
@@ -10041,6 +10042,25 @@
       "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
       "dev": true
     },
+    "node_modules/linkify-it": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-6.1.0.tgz",
+      "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/markdown-it"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "uc.micro": "^3.0.0"
+      }
+    },
     "node_modules/loader-runner": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
@@ -10498,6 +10518,61 @@
         "convex-hull": "^1.0.3"
       }
     },
+    "node_modules/markdown-it": {
+      "version": "15.0.0",
+      "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-15.0.0.tgz",
+      "integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/markdown-it"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^3.0.0",
+        "entities": "^8.0.0",
+        "linkify-it": "^6.0.0",
+        "mdurl": "^2.1.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^3.0.0"
+      },
+      "bin": {
+        "markdown-it": "bin/markdown-it.mjs"
+      }
+    },
+    "node_modules/markdown-it/node_modules/argparse": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-3.0.0.tgz",
+      "integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/puzrin"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/nodeca"
+        }
+      ],
+      "license": "Python-2.0"
+    },
+    "node_modules/markdown-it/node_modules/entities": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz",
+      "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=20.19.0"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
     "node_modules/markdown-table": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
@@ -10573,6 +10648,12 @@
       "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
       "dev": true
     },
+    "node_modules/mdurl": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.1.0.tgz",
+      "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
+      "license": "MIT"
+    },
     "node_modules/media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -12693,6 +12774,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/px2rem": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/px2rem/-/px2rem-0.5.0.tgz",
@@ -15055,6 +15145,12 @@
         "dup": "^1.0.0"
       }
     },
+    "node_modules/uc.micro": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-3.0.0.tgz",
+      "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
+      "license": "MIT"
+    },
     "node_modules/undici-types": {
       "version": "7.12.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",
@@ -24154,6 +24250,14 @@
       "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
       "dev": true
     },
+    "linkify-it": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-6.1.0.tgz",
+      "integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
+      "requires": {
+        "uc.micro": "^3.0.0"
+      }
+    },
     "loader-runner": {
       "version": "4.3.1",
       "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
@@ -24539,6 +24643,31 @@
         "convex-hull": "^1.0.3"
       }
     },
+    "markdown-it": {
+      "version": "15.0.0",
+      "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-15.0.0.tgz",
+      "integrity": "sha512-Lf8ajvVNdRpzSNB4VegxNy7gjs8gU35l4b4+ET49LrQC5PKYwLZ72u60LeJ9gv3qiaesuYjJWCyVeQmv/QWKQw==",
+      "requires": {
+        "argparse": "^3.0.0",
+        "entities": "^8.0.0",
+        "linkify-it": "^6.0.0",
+        "mdurl": "^2.1.0",
+        "punycode.js": "^2.3.1",
+        "uc.micro": "^3.0.0"
+      },
+      "dependencies": {
+        "argparse": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmmirror.com/argparse/-/argparse-3.0.0.tgz",
+          "integrity": "sha512-BOp5NMrHqKxmq/OLr+clzzrRxgOKSLkcjmkWuChp7Irqwn4s74WjOBPIgWfA/HMcBnVkZ5XEuf9uUqzlpfCQ6A=="
+        },
+        "entities": {
+          "version": "8.0.0",
+          "resolved": "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz",
+          "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="
+        }
+      }
+    },
     "markdown-table": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
@@ -24604,6 +24733,11 @@
       "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
       "dev": true
     },
+    "mdurl": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-2.1.0.tgz",
+      "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg=="
+    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -26190,6 +26324,11 @@
       "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
       "dev": true
     },
+    "punycode.js": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmmirror.com/punycode.js/-/punycode.js-2.3.1.tgz",
+      "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="
+    },
     "px2rem": {
       "version": "0.5.0",
       "resolved": "https://registry.npmjs.org/px2rem/-/px2rem-0.5.0.tgz",
@@ -28061,6 +28200,11 @@
         "dup": "^1.0.0"
       }
     },
+    "uc.micro": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-3.0.0.tgz",
+      "integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw=="
+    },
     "undici-types": {
       "version": "7.12.0",
       "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz",

+ 1 - 0
package.json

@@ -33,6 +33,7 @@
     "jspdf": "^2.5.2",
     "kde": "^0.0.1",
     "lottie-web": "^5.12.2",
+    "markdown-it": "^15.0.0",
     "nprogress": "^0.2.0",
     "particles.js": "^2.0.0",
     "plotly.js": "^2.35.3",

BIN
src/assets/bg/ai_robot.gif


BIN
src/assets/bg/deepseek_role.webp


BIN
src/assets/bg/robot_bg.webp


+ 541 - 0
src/components/AiRobot.vue

@@ -0,0 +1,541 @@
+<template>
+  <div class="ai_main">
+      <div class="main_ai_content" @scroll="handleUserScroll">
+          <div class="main_ai_weclome" >
+            <h1>HI~我是慧教研大数据精准分析AI助教</h1>
+            <p>您身边的智能助教,专注于成绩数据分析与教学改进建议,为您提供精准解读与决策支持,快来体验吧~</p>
+          </div>
+          <div class="main_ai_list" v-if="isShowQuickList">
+            <div class="main_ai_list_title">
+              您可以这样问:
+            </div>
+            <div class="main_ai_list_content">
+                <span class="list_item_tag" v-for="(value,index) in quickList" :key="'list_quick_'+index" @click="quickAsk(value.title)">
+                {{value.title}}
+                </span>
+            </div>
+          </div>
+         <!-- 显示对话历史 -->
+          <div class="chat_history" v-if="chatHistory.length > 0 ">
+            <div   v-for="(message, index) in chatHistory" 
+              :key="index" 
+              :class="['message_item', message.role]"
+            >
+              <div class="message_user" v-if="message.role === 'user'">
+                
+              </div>
+              <div v-else class="message_avatar"> 
+                <div class="avatar_icon">
+                </div>
+                <div class="avatar_name"> 
+                  <div class="name_text">
+                    deepseek-v4-pro
+                    <span v-if="loading">正在思考中,请稍后<i class="el-icon-loading"></i></span>
+                    <span v-else>回答完成</span>
+                  </div>
+                </div>
+              </div>
+              <div class="user_message_content" v-if="message.role === 'user'">
+                <div class="content_user" >
+                  {{message.content}}
+                </div>
+              </div>
+              <div class="assistant_message_content" v-else>
+                <div class="content_text"  v-html="FormatContent(message.content, message.role)"></div>
+              </div>
+            </div>
+          </div>
+      </div>
+      <div class="page_jg_20"></div>
+      <div class="main_ai_input">
+        <el-input
+          type="textarea"
+          :rows="4"
+          :maxlength="1000"
+          placeholder="解读交给我,有问必答"
+          v-model="inputContent"
+          @keydown.native.enter="handleEnter"
+          >
+        </el-input>
+        <div class="chat_model" >
+          <i class="iconfont icon_deepseek"></i>
+          deepseek-v4-pro
+        </div>
+        <div class="input_button" @click="StopMessage" v-if="loading" title="点击可停止思考">
+          <i class="el-icon-loading"></i>
+        </div>
+        <div class="input_button" @click="SendMessage" v-else>
+          <i class="iconfont icon_shangjiantou"></i>
+        </div>
+        <!-- <div class="file_button" @click="OpenMessage()">
+          <i class="iconfont icon_folder_upload"></i>
+        </div> -->
+      </div>
+      <div class="main_ai_message">
+        内容由ai生成,仅供参考
+      </div>
+      <div class="page_jg_10"></div>
+    </div>
+</template>
+<script>
+import MarkdownIt from 'markdown-it';
+export default {
+  name: "AiRobot",
+  components: {
+
+  },
+  props: {
+    
+
+   isShowQuickList:{
+     type:Boolean,
+     default:true
+   },//默认显示快捷列表
+    
+    
+  },
+  data() {
+    return {
+      inputContent:'',//输入框
+      chatHistory:[],//聊天历史记录
+      loading:false,//加载动画
+      apiKey:process.env.VUE_APP_DEEPSEEK_API_KEY || 'sk-85322fa189f24dd7aae324c5ff6a236a',// 从环境变量获取API密钥
+      quickList:[
+        {
+          title:'分数段如何助力教学问题诊断?',
+        },
+        {
+          title:'平均分波动背后,教学调整方向在哪?',
+        },
+        {
+          title:'标准差在成绩分析里怎么用?能看出啥教学问题?',
+        },
+        {
+          title:'怎样通过名次段洞察学生学习走势?',
+        },
+        {
+          title:'等级分布在教学分层决策中的应用?',
+        },
+        {
+          title:'借助得分率,怎样精准定位学生知识薄弱点?',
+        },
+        {
+          title:'超均分指标,如何指导教学资源分配?',
+        },
+        {
+          title:'运用难度指标,怎样科学把控教学深度?',
+        },
+        {
+          title:'用n%名段,如何精准规划教学?',
+        },
+        {
+          title:'信度指标能帮助我们排查教学中的哪些隐患?',
+        },
+        {
+          title:'在教学质量评估中,标准差能为教师教学的精准度提供哪些数据?',
+        },
+        {
+          title:'借助标准分,如何横向比较学生不同学科的学习表现?',
+        },
+        {
+          title:'优秀率的波动能反映出教学在培养尖子生方面的哪些问题?',
+        },
+        {
+          title:'历次平均分变化,能为教学调整提供哪些线索?',
+        },
+        {
+          title:'历次标准分变化,对教学评价体系有何调整意义?',
+        },
+        {
+          title:'区分度指标,怎样助力实现个性化教学?',
+        },
+        {
+          title:'通过对比不同学科的 n% 名段,能发现学生学科学习的哪些不均衡问题?',
+        },
+        {
+          title:'如何借助四分位盒须图分析成绩离散问题?',
+        },
+        {
+          title:'通过分析历次标准分,怎样判断教学是否有效提升了学生在整体中的竞争力?',
+        },
+        {
+          title:'从赋分等级分布能看出哪些教学问题?'
+        }
+      ],//快捷提问列表
+      currentStreamingMessageIndex: 0,//当前正在流式传输的消息序号
+      abortController: null,//用于取消请求的控制器
+      isUserScrolling: false,//是否正在用户滚动页面
+
+    };
+  },
+  watch: {
+    
+  },
+  mounted()
+  {
+
+  },
+  methods: 
+  {
+    //停止消息
+    StopMessage()
+    {
+        // 取消当前正在进行的请求
+        if (this.abortController) {
+            this.abortController.abort();
+            this.abortController = null;
+        }
+        
+        // 结束流式传输消息
+        this.finishStreamingMessage();
+        
+        // 停止加载状态
+        this.loading = false;
+        this.$emit('StopMessage');
+        // 如果当前有正在流式传输的消息,将其标记为完成
+        if (this.currentStreamingMessageIndex >= 0 && 
+            this.currentStreamingMessageIndex < this.chatHistory.length) {
+            this.$set(this.chatHistory[this.currentStreamingMessageIndex], 'isStreaming', false);
+            this.currentStreamingMessageIndex = -1;
+        }
+    },
+
+    //打开消息提醒
+    OpenMessage()
+    {
+        this.$message({
+          message: '上传文件功能暂未开放,敬请期待!',
+          type: 'warning'
+        });
+    },
+    // 发送消息
+    async SendMessage()
+    {
+      // 确保 inputContent 是字符串类型
+      const content = this.inputContent !== null && this.inputContent !== undefined ? 
+        String(this.inputContent) : '';
+      
+      if (!content.trim()) 
+      {
+        this.$message({
+          message: '您还未输入内容哦!',
+          type: 'warning'
+        });
+        return;
+      }
+     
+      
+      const userMessage = this.inputContent.trim();
+      this.addMessage('user', userMessage);
+      this.inputContent = '';
+      this.loading = true;
+      
+      try {
+        const response = await this.callDeepSeekAPI(userMessage);
+        const aiResponse = response.data.choices[0].message.content;
+
+        console.log('AI 返回的消息:', aiResponse);
+        this.addMessage('assistant', aiResponse);
+      } catch (error) {
+        console.error('API调用失败:', error);
+        let errorMessage = '抱歉,AI助手暂时无法回答您的问题。';
+        if (error.response) {
+          switch (error.response.status) {
+            case 401:
+              errorMessage = 'API密钥无效,请检查配置。';
+              break;
+            case 429:
+              errorMessage = '请求过于频繁,请稍后再试。';
+              break;
+            case 500:
+              errorMessage = '服务器内部错误,请稍后再试。';
+              break;
+            default:
+              errorMessage = `请求失败: ${error.response.data.message || '未知错误'}`;
+          }
+        }
+        // this.addMessage('assistant', errorMessage);
+      } finally {
+        this.loading = false;
+      }
+    },
+
+    // 设置输入框内容
+    SetInputContent(content)
+    {
+      if(content)
+      {
+        this.inputContent = content;
+        // 如果需要自动发送,可以调用发送方法
+        this.SendMessage();
+        this.inputContent = '';
+      }
+      
+    },
+
+    // 快捷提问
+    quickAsk(question) {
+      this.inputContent = question;
+      this.SendMessage();
+    },
+
+    // 调用DeepSeek API(流式)
+    async callDeepSeekAPI(message) {
+      const apiUrl = 'https://api.deepseek.com/chat/completions';
+      // 创建AbortController用于取消请求
+      this.abortController = new AbortController();
+      
+      // 构建消息历史(只保留最近几条以控制token使用)
+      const recentMessages = this.chatHistory.slice(-6); // 保留最近6轮对话
+      const messages = [
+        {
+          role: "system", //用户角色信息
+          content: "你是一位精通分析的教育专家,请帮我分析这份考试数据,要求保持原文核心逻辑和数据画像框架,语言更简洁精准、通俗易懂,贴合教育数据分析诊断的专业视角,同时确保上下文流畅,突出问题定位和教学改进相关的核心导向。"//角色提示词信息
+        },
+        ...recentMessages
+          .filter(msg => !msg.isStreaming) // 过滤掉仍在流式传输的消息
+          .map(msg => ({
+            role: msg.role,
+            content: msg.content
+          })),
+        {
+          role: "user",
+          content: message
+        }
+      ];
+      
+      const payload = {
+        model: "deepseek-v4-pro",//模型名称  deepseek-v4-pro 思考版   DeepSeek-V4-Flash-0731 不带思索版
+        thinking: {
+          type: "disabled"//如果设为 enabled,则使用思考模式。如果设为 disabled,则使用非思考模式
+        },
+        messages: messages,
+        stream: true,// 是否为流式消息
+      };
+
+      // 添加AI助手的初始消息
+      this.currentStreamingMessageIndex = this.chatHistory.length;
+      this.addStreamingMessage('assistant', '');
+      
+      try {
+        const response = await fetch(apiUrl, {
+          method: 'POST',
+          headers: {
+            'Content-Type': 'application/json',
+            'Authorization': `Bearer ${this.apiKey}`
+          },
+          body: JSON.stringify(payload),
+          signal: this.abortController.signal // 添加信号用于取消请求
+        });
+        console.log('打印请求结果response', response);
+        if (!response.ok) {
+          throw new Error(`HTTP error! status: ${response.status}`);
+        }
+        
+        const reader = response.body.getReader();
+        const decoder = new TextDecoder('utf-8');
+        let accumulatedContent = '';
+        
+        while (true) {
+            // 检查是否已取消请求
+            if (this.abortController.signal.aborted) {
+                break;
+            }
+          const { done, value } = await reader.read();
+          if (done) break;
+          
+          const chunk = decoder.decode(value, { stream: true });
+          const lines = chunk.split('\n');
+          
+          for (const line of lines) {
+            if (this.abortController.signal.aborted) {
+                break;
+            }
+            if (line.startsWith('data: ')) {
+              const data = line.slice(6);
+              if (data === '[DONE]') {
+                // 流式传输完成
+                this.finishStreamingMessage();
+                return;
+              }
+              
+              try {
+                const parsed = JSON.parse(data);
+                const content = parsed.choices[0]?.delta?.content || '';
+                if (content) {
+                  accumulatedContent += content;
+                  this.updateStreamingMessage(accumulatedContent);
+                }
+              } catch (e) {
+                // 忽略解析错误
+                console.warn('Failed to parse stream data:', e);
+              }
+            }
+          }
+        }
+        
+        this.finishStreamingMessage();
+      } catch (error) {
+        // 检查是否是因为取消请求导致的错误
+        if (error.name === 'AbortError') {
+            console.log('请求已被用户取消');
+            this.finishStreamingMessage();
+            return; // 不抛出错误,因为这是用户主动取消的
+        }
+        console.error('Stream error:', error);
+        this.finishStreamingMessage();
+        throw error;
+      }
+    },
+
+
+    // 添加流式消息
+    addStreamingMessage(role, content) {
+      const now = new Date();
+      const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
+      
+      this.chatHistory.push({
+        role: role,
+        content: content,
+        time: timeString,
+        isStreaming: true
+      });
+      
+      this.scrollToBottom();
+    },
+
+    // 更新流式消息内容
+    updateStreamingMessage(content) {
+      if (this.currentStreamingMessageIndex >= 0 && 
+          this.currentStreamingMessageIndex < this.chatHistory.length) {
+        this.$set(this.chatHistory[this.currentStreamingMessageIndex], 'content', content);
+        this.scrollToBottom();
+      }
+    },
+
+    // 完成流式消息
+    finishStreamingMessage() {
+      if (this.currentStreamingMessageIndex >= 0 && 
+          this.currentStreamingMessageIndex < this.chatHistory.length) {
+        this.$set(this.chatHistory[this.currentStreamingMessageIndex], 'isStreaming', false);
+        this.currentStreamingMessageIndex = -1;
+      }
+      // 完成后重置用户滚动状态
+      this.isUserScrolling = false;
+      this.scrollToBottom();
+    },
+
+    // 添加消息到历史记录
+    addMessage(role, content) {
+      const now = new Date();
+      const timeString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
+      
+      this.chatHistory.push({
+        role: role,
+        content: content,
+        time: timeString
+      });
+      
+      // 滚动到底部
+      this.$nextTick(() => {
+        const contentElement = document.querySelector('.main_ai_content');
+        if (contentElement) {
+          contentElement.scrollTop = contentElement.scrollHeight;
+        }
+      });
+    },
+
+    // 滚动到底部
+    scrollToBottom() {
+      this.$nextTick(() => {
+        const contentElement = document.querySelector('.main_ai_content');
+        if (contentElement) {
+          // 只有在用户未手动滚动或已接近底部时才自动滚动
+          const { scrollTop, scrollHeight, clientHeight } = contentElement;
+          const isNearBottom = scrollHeight - scrollTop - clientHeight < 50; // 距离底部50px以内
+          
+          // 如果用户未手动滚动或者已经接近底部,则自动滚动
+          if (!this.isUserScrolling || isNearBottom) {
+            contentElement.scrollTop = scrollHeight; // 修正:使用scrollHeight而不是contentHeight
+            this.isUserScrolling = false; // 重置滚动状态
+          }
+        }
+      });
+    },
+
+    // 监听用户滚动事件
+    handleUserScroll() {
+      const contentElement = document.querySelector('.main_ai_content');
+      if (contentElement) {
+        const { scrollTop, scrollHeight, clientHeight } = contentElement;
+        const isNearBottom = scrollHeight - scrollTop - clientHeight < 50;
+        
+        // 如果用户滚动到顶部附近,标记为用户正在滚动
+        if (!isNearBottom) {
+          this.isUserScrolling = true;
+        } else {
+          // 如果滚动到底部附近,取消用户滚动标记
+          this.isUserScrolling = false;
+        }
+      }
+    },
+
+    // 格式化消息内容
+    FormatContent(content, user) 
+    {
+       console.log("打印用户的消息",user);
+        if (user === 'user') {
+            // 用户消息只显示前100个字,超出部分用省略号替代
+            console.log("打印用户的消息",content.length);
+            if(content.length>100) 
+            {
+              return this.escapeHtml(content.substring(0, 100) + '...');
+            }
+            else
+            {
+              return this.escapeHtml(content);
+            }
+            
+        }
+         const md = new MarkdownIt({
+            html: true,// 允许解析HTML标签,可以直接渲染HTML代码
+            linkify: true,// 自动识别URL并将其转换为可点击的链接
+            typographer: true,//启用语言替换和引号美化功能,如将引号转换为更美观的样式
+        });
+        return md.render(content);
+    },
+
+
+
+
+    // 转义HTML特殊字符
+    escapeHtml(text) {
+      const map = {
+        '&': '&amp;',
+        '<': '&lt;',
+        '>': '&gt;',
+        '"': '&quot;',
+        "'": '&#039;'
+      };
+      
+      return text.replace(/[&<>"']/g, function(m) { return map[m]; });
+    },
+
+
+    // 处理回车键发送(任何情况下的回车都发送消息)
+    handleEnter(event) {
+        // 如果按住 Shift 键,则允许换行(不阻止默认行为)
+        if (event.shiftKey) {
+            return; // 允许默认的换行行为
+        }
+        
+        // 如果没有按住 Shift 键,则发送消息
+        event.preventDefault();
+        this.SendMessage();
+    }
+  },
+};
+</script>
+<style lang="scss" scoped>
+
+</style>

+ 408 - 1
src/styles/common.scss

@@ -10713,4 +10713,411 @@ body {
     background-position: 50% 50%;
 
   }
-}
+}
+
+//Ai组件对话框样式
+.ai_main
+{
+    width: 100%;
+    height: calc(100% - 40px);
+
+    
+    .main_ai_weclome
+    {
+      width: calc(100% - 40px);
+      margin: auto;
+      height: auto;
+      text-align: center;
+      // padding: 20px;
+      box-sizing: border-box;
+      h1{
+        font-weight: bold;
+        font-size: 28px;
+        line-height: 32px;
+        background: linear-gradient(90deg, #2E3FFA 0%, #A246FF 100%); // 蓝色渐变
+        -webkit-background-clip: text;
+        -webkit-text-fill-color: transparent;
+        background-clip: text;
+        text-fill-color: transparent;
+      }
+
+      p
+      {
+        font-weight: 400;
+        font-size: 14px;
+        color: #666666;
+        line-height: 20px;
+      }
+    }
+
+    .main_ai_content
+    {
+      width: 100%;
+      // min-height: 200px;
+      // height: calc(100vh - 340px);
+      height: calc(100vh - 180px);
+      overflow-y: auto;
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      .chat_history
+      {
+        width: calc(100% - 40px);
+        margin: auto;
+        height: auto;
+
+      }
+      .message_item 
+      {
+        
+        font-size: 16px;
+        line-height: 30px;
+        color:#333;
+        margin-bottom: 20px;
+        .message_avatar
+        {
+          width: 100%;
+          height: 30px;
+          display: flex;
+          justify-content: flex-start;
+
+          .avatar_icon
+          {
+            width: 32px;
+            height: 32px;
+            background-image: url("../assets/bg/deepseek_role.webp");
+            background-repeat: no-repeat;
+            background-size:32px 32px;
+            background-position: 0% 0%;
+          }
+
+          .avatar_name
+          {
+            width: auto;
+            margin-left: 10px;
+            font-weight: 400;
+            font-size: 12px;
+            color: #9646FF;
+
+            .name_text
+            {
+              background: #F8F8FF;
+              border-radius: 10px 10px 10px 10px;
+              padding: 0 10px;
+              
+            }
+          }
+        }
+
+        .content_text 
+        {
+           width: calc(100% - 60px);
+           margin-left: 40px;
+          border-radius: 0 8px 8px 8px;
+          text-align: left;
+          min-height: 20px;
+            
+            // 格式化内容样式
+          
+              h1, h2, h3 {
+                margin: 0px 0px;
+                font-weight: 600;
+                color: #333;
+              }
+              
+              h1 {
+                font-size: 20px;
+                border-bottom: 1px solid #eee;
+                padding-bottom: 5px;
+              }
+              
+              h2 {
+                font-size: 18px;
+              }
+              
+              h3 {
+                font-size: 16px;
+              }
+              
+              strong {
+                font-weight: 600;
+                color: #333;
+              }
+              
+              em {
+                font-style: italic;
+              }
+              
+              ul, ol {
+                padding-left: 20px;
+                margin: 10px 0;
+              }
+              
+              li {
+                margin: 5px 0;
+                line-height: 20px;
+              }
+              
+              code {
+                font-family: 'Courier New', monospace;
+                background-color: #f0f0f0;
+                padding: 2px 4px;
+                border-radius: 3px;
+                font-size: 14px;
+              }
+              
+              .code-block {
+                background-color: #2d2d2d;
+                color: #f8f8f2;
+                padding: 15px;
+                border-radius: 5px;
+                margin: 10px 0;
+                overflow-x: auto;
+                
+                code {
+                  background-color: transparent;
+                  color: inherit;
+                  padding: 0;
+                }
+              }
+              
+              .inline-code {
+                background-color: #f0f0f0;
+                color: #333;
+                padding: 2px 4px;
+                border-radius: 3px;
+                font-size: 14px;
+              }
+              
+              br {
+                line-height: 1;
+              }
+            
+        }
+
+        .content_user
+        {
+          width: 584px;
+          height: auto;
+          background: linear-gradient( 90deg, #E1E0FF 0%, #EAE4FF 100%);
+          border-radius: 10px 10px 10px 10px;
+          padding:5px 10px;
+          box-sizing: border-box;
+          line-height: 30px;
+          // 添加以下样式来限制只显示前2行
+          display: -webkit-box;
+          -webkit-line-clamp: 2;
+          -webkit-box-orient: vertical;
+          overflow: hidden;
+          text-overflow: ellipsis;
+        }
+
+        .user_message_content
+        {
+          display: flex;
+          justify-content: flex-end;
+        }
+
+        .assistant_message_content
+        {
+          text-align: left;
+        }
+  
+      }
+
+    }
+
+    .main_ai_list
+    {
+      width: calc(100% - 40px);
+      margin: auto;
+      height: auto;
+      margin-top: 10px;
+      margin-bottom: 20px;
+      .main_ai_list_title
+      {
+        width: 100%;
+        height: auto;
+        font-weight: bold;
+        font-size: 14px;
+        color: #333333;
+        line-height: 40px;
+      }
+
+      .main_ai_list_content
+      {
+        width: 100%;
+
+        height: auto;
+        display: flex;
+        justify-content: flex-start;
+        flex-wrap: wrap;
+        gap: 10px;
+      }
+
+      .list_item_tag
+      {
+        width: calc(50% - 20px);
+        min-height: 32px;
+        line-height: 20px;
+        text-align: left;
+        background: linear-gradient( 90deg, #EAEFFF 0%, #F2F0FF 100%);
+
+        border-radius: 6px 6px 6px 6px;
+        padding: 5px 10px;
+        box-sizing: border-box;
+        cursor: pointer;
+        display: flex;
+        // justify-content: center;
+        align-items: center;
+      }
+    }
+    .main_ai_input
+    {
+      width: calc(100% - 40px);
+      margin: auto;
+      height: auto;
+      background: #FFFFFF;
+      border-radius: 20px 20px 20px 20px;
+      // border: 1px solid;
+      // border-image: linear-gradient(90deg, rgba(46, 63, 250, 1), rgba(162, 70, 255, 1)) 1 1;
+      position: relative;
+      .el-textarea__inner
+      {
+        border: 1px solid #A246FF;
+        border-radius: 20px 20px 20px 20px;
+        // border-image: linear-gradient(90deg, rgba(46, 63, 250, 1), rgba(162, 70, 255, 1)) 1 1;
+        font-weight: 400;
+        font-size: 14px;
+        color: #333;
+        line-height: 25px;
+        min-height: 130px;
+      }
+
+      .input_button
+      {
+        position: absolute;
+        right:10px;
+        bottom: 10px;
+        width: 30px;
+        height: 30px;
+        border-radius: 50%;
+        background: #2E3FFA;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        i{
+          color:#fff;
+          font-size: 16px;
+        }
+
+      }
+
+      .chat_model
+      {
+        position: absolute;
+        left: 10px;
+        bottom: 10px;
+        width: 120px;
+        height: 30px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 5px;
+        background: #F0F1FF;
+        border-radius: 10px 10px 10px 10px;
+        font-family: PingFang SC, PingFang SC;
+        font-weight: 400;
+        font-size: 12px;
+        color:#A246FF;
+
+       
+      }
+
+      //上传文件
+      .file_button
+      {
+        position: absolute;
+        right:60px;
+        bottom: 10px;
+        width: 30px;
+        height: 30px;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+        i{
+          color:#666;
+          font-size: 30px;
+        }
+      }
+    }
+
+    .main_ai_message
+    {
+      font-weight: 400;
+      font-size: 12px;
+      color: #C0C4CC;
+      text-align: center;
+      line-height: 30px;
+    }
+}
+
+// ai抽屉
+.ai_robot_drawer
+{
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  display: flex;
+  justify-content: space-between;
+  .drawer_left
+  {
+    width: 160px;
+    height: 100%;
+    background-color: #fff;
+    position: relative;
+    .robot_head
+    {
+      width: 100%;
+      height: auto;
+      img
+      {
+        width: 100%;
+        height: auto;
+      }
+    }
+
+    .robot_go_home
+    {
+      width: 120px;
+      height: 40px;
+      position: absolute;
+      bottom: 20px;
+      left: 20px;
+      background: #2E64FA;
+      border-radius: 10px 10px 10px 10px;
+
+      font-weight: 400;
+      font-size: 14px;
+      color: #FFFFFF;
+      line-height: 40px;
+      text-align: center;
+      cursor: pointer;
+    }
+  }
+
+  .drawer_right
+  {
+    width: calc(100% - 160px);
+    height: 100%;
+    // background: linear-gradient( 180deg, #F5F4F4 80%, rgba(245,244,244,0) 100%);
+    border-radius: 0px 0px 0px 0px;
+
+    background: url("../assets/bg/robot_bg.webp");
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    background-position: 0% 0%;
+  }
+}

+ 75 - 34
src/views/targetImprove/index.vue

@@ -9,50 +9,48 @@
       
       <UserMain />
     </div>
+    <div>
+      <el-drawer title="Ai助教" :visible.sync="showAiDrawer" :direction="direction" :withHeader="false"
+        :before-close="CloseAiDrawer" size="70%">
+        <div class="ai_robot_drawer">
+          <div class="drawer_left">
+            <div class="robot_head">
+              <img src="@/assets/bg/ai_robot.gif" alt=""></img>
+            </div>
+            <!-- <div class="robot_go_home" @click="GoAiHome">
+              去主页
+            </div> -->
+          </div>
+          <div class="drawer_right">
+            <AiChat :isShowQuickList="false" ref="aiRobot"></AiChat>
+          </div>
+        </div>
+      </el-drawer>
+    </div>
   </div>
 </template>
 <script>
 import UserMain from "./components/UserMain"//主页面
 
 import Header from "../layout/components/Header.vue"//头部
-
+import AiChat from '@/components/AiRobot.vue'//ai智能对话组件
 export default {
   components: {
     UserMain,
-    Header
+    Header,
+    AiChat
+  },
+  provide() {
+    return {
+      openAiRobot: this.OpenAiRobot
+    }
   },
   data() {
     return {
-      userInfo: {},
-      filtersData:[
-        {
-          name:'科目名称',
-          value:'',
-          list:[
-            {
-              label:'语文',
-              value:"",
-            },
-            {
-              label:'数学',
-              value:"",
-            },
-            {
-              label:'英语',
-              value:"",
-            },
-            {
-              label:'物理',
-              value:"",
-            },
-            {
-              label:'化学',
-              value:"",
-            },
-            
-          ],
-        },
-      ]
+      showAiDrawer:false,//是否显示ai对话框
+      direction: 'rtl',//对话框方向
+      isAiGenerating: false, // 标记AI是否正在生成内容
+
     }
   },
   computed:{},
@@ -61,8 +59,51 @@ export default {
      
   },
   methods: { 
-    // 筛选组件回调
-    ChangeFilters()
+    //关闭ai对话框
+    CloseAiDrawer()
+    {
+      this.showAiDrawer=false;
+    },
+
+    //打开ai对话框
+    OpenAiRobot(content) 
+    {
+      this.showAiDrawer = true;
+      console.log("打印打开ai对话框",);
+      if (this.$refs.aiRobot && typeof this.$refs.aiRobot.SetInputContent === 'function') {
+        this.isAiGenerating = this.$refs.aiRobot.loading;
+      }
+
+      // 如果AI正在生成内容,则不发起新请求
+      if (this.isAiGenerating) {
+        console.log("AI正在生成内容,请稍后再试");
+        this.$message.alert("AI正在生成内容,请稍后再试");
+        return;
+      }
+      // 设置AI正在生成的标志
+      this.isAiGenerating = true;
+      if (content) {
+        console.log("打开ai对话框", content);
+        // this.$refs.aiRobot.SetInputContent(content);
+        // 添加安全检查
+        this.$nextTick(() => {
+          if (this.$refs.aiRobot && typeof this.$refs.aiRobot.SetInputContent === 'function') {
+            this.$refs.aiRobot.SetInputContent(content);
+          } else {
+            console.warn('aiRobot 组件引用或 SetInputContent 方法不存在');
+          }
+        });
+      }
+    },
+
+    //去主页
+    GoAiHome()
+    {
+
+    },
+
+    //停止对话
+    StopMessage()
     {
 
     },

+ 10 - 0
src/views/targetImprove/practice/index.vue

@@ -46,12 +46,14 @@ export default {
   computed:{
     ...mapState('target', ['subjectCode'])
   },
+  inject: ['openAiRobot'],//注入Ai机器人组件调用方法
   watch:{
     // 监听顶部的subjectCode变化
     subjectCode(newVal, oldVal){
       console.log('科目编码改变了', newVal, oldVal)
       // ========= 业务逻辑写这里 =========
       // 例如:切换科目,重置步骤、重新请求知识点图谱数据
+      this.OpenAiRobot();
       
     }
   },
@@ -61,6 +63,14 @@ export default {
 
   },
   methods: { 
+
+
+    // 打开Ai机器人测试
+    OpenAiRobot()
+    {
+      let content = "使用Ai机器人测试,帮我解答这道题";
+      this.openAiRobot(content);
+    },
     
 
     // 步骤切换