浏览代码

feat: add AI生成解释功能并补充多语言文案

1. 新增多语言国际化文案,包含生成中、请稍后重试等提示
2. 新增SSE流式对话工具方法,实现AI流式返回生成内容
3. 在选择题和问答设置页面添加AI生成解释按钮,支持加载和生成状态控制
4. 适配多语言环境下的界面显示逻辑
lsc 15 小时之前
父节点
当前提交
f389efb5e1

+ 61 - 4
src/components/pages/workPage/components/setChoiceQuestion.vue

@@ -136,17 +136,17 @@
       <div class="explanation">
         <div class="e_header">
           <span>{{ lang.ssExplanationTip }}</span>
-          <div v-show="false">
+          <div @click="generateExplanation(item, index)" :disabled="chatLoading[index]" v-loading="messagesloading[index]">
             <svg t="1774233113566" viewBox="0 0 1027 1024" width="200" height="200">
               <path
-                d="M511.979521 544.586217a54.525819 54.525819 0 0 1-23.359066-5.247791L31.038758 321.427143a54.397824 54.397824 0 0 1 0-98.236071L488.620455 5.279789c14.719411-7.039718 31.99872-7.039718 46.718131 0l457.581697 217.911283a54.397824 54.397824 0 0 1 0 98.236071l-457.581697 217.911283A54.525819 54.525819 0 0 1 511.979521 544.586217zM180.984761 272.341106L511.979521 429.966801l331.058757-157.625695L511.979521 114.587417 180.984761 272.341106z m354.417823 746.402144l457.517699-217.911283a54.397824 54.397824 0 1 0-46.718131-98.236071L511.979521 909.371625l-434.158634-206.775729a54.397824 54.397824 0 0 0-46.718131 98.236071l457.581697 217.911283a54.525819 54.525819 0 0 0 46.718131 0z m0-239.73441l457.517699-217.847286a54.397824 54.397824 0 1 0-46.718131-98.300068L511.979521 669.573217l-434.158634-206.711731a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z"
+                d="M511.979521 544.586217a54.525819 54.525819 0 0 1-23.359066-5.247791L31.038758 321.427143a54.397824 54.397824 0 0 1 0-98.236071L488.620455 5.279789c14.719411-7.039718 31.99872-7.039718 46.718131 0l457.581697 217.911283a54.397824 54.397824 0 0 1 0 98.236071l-457.581697 217.911283A54.525819 54.525819 0 0 1 511.979521 544.586217zM180.984761 272.341106L511.979521 429.966801l331.058757-157.625695L511.979521 114.587417 180.984761 272.341106z m354.417823 746.402144l457.517699-217.911283a54.397824 54.397824 0 1 0-46.718131-98.236071L511.979521 909.371625l-434.158634-206.775729a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z m0-239.73441l457.517699-217.847286a54.397824 54.397824 0 1 0-46.718131-98.300068L511.979521 669.573217l-434.158634-206.711731a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z"
                 p-id="9073"></path>
             </svg>
-            <span>{{ lang.ssAIGenerate }}</span>
+            <span>{{ chatLoading[index] ? lang.ssGenerating : lang.ssAIGenerate }}</span>
           </div>
         </div>
         <el-input type="textarea" rows="3" style="font-size: 1.2rem;" resize="none" v-model="item.explanation"
-          :placeholder="lang.ssEnterExplanation" @input="setTestJson()"></el-input>
+          :placeholder="lang.ssEnterExplanation" v-loading="messagesloading[index]" :disabled="chatLoading[index]" @input="setTestJson()"></el-input>
       </div>
     </div>
 
@@ -167,7 +167,10 @@
 <script>
 import { v4 as uuidv4 } from 'uuid';
 import { uploadOneFile } from '@/components/tools/uploadFile'
+import { myMixin } from '@/mixins/mixin'
+
 export default {
+  mixins: [myMixin],
   emits: ['setTestJson', 'save'],
   props: {
     workData: {
@@ -180,6 +183,10 @@ export default {
       jsonData: {},
       dragItem: null,
       delJson:null,
+      streamController: [],
+      chatLoading: [],
+      messagesloading: [],
+      userid: this.$route.query.userid,
     }
   },
   watch: {
@@ -427,6 +434,56 @@ export default {
         this.delJson = null;
       }
     },
+    // 生成解释
+    async generateExplanation(item, index) {
+      if (this.chatLoading[index]) {
+        this.$message.warning(this.lang.ssPleaseWait);
+        return
+      }
+      if (this.messagesloading[index]) {
+        this.$message.warning(this.lang.ssPleaseWait);
+        return
+      }
+      let zimu = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+      let prompt = `- 题干:${item.teststitle}
+      - 选项:  
+        ${item.checkList.map((check, index) => `${zimu[index]}. ${check}`).join('\n')}
+      - 正确答案(若已知):${item.answer.length > 0 ? item.answer.map(index => zimu[index]).join('、') : '未知'}`
+      this.chatLoading[index] = true
+      this.messagesloading[index] = true
+      this.$forceUpdate()
+      this.chat_stream(prompt, '7dc8f9f4-415e-4cc6-a572-68bd758e9c8b', this.userid || this.workData.userid, this.lang.lang, (event) => {
+        if (event.type === 'message') {
+          this.jsonData.testJson[index].explanation = event.data
+          this.messagesloading[index] = false
+        }
+        else if (event.type === 'messageEnd') {
+          this.jsonData.testJson[index].explanation = event.data
+          this.messagesloading[index] = false
+          this.chatLoading[index] = false
+          this.$forceUpdate()
+        }else if (event.type === 'error') {
+          this.chatLoading[index] = false
+          this.messagesloading[index] = false
+          this.$message.error(event.data)
+        }
+      }).then(controller => {
+        this.streamController[index] = controller
+      }).catch(err => {
+        this.chatLoading[index] = false
+        this.messagesloading[index] = false
+        console.log('err', err)
+        this.stopMessage(index)
+      })
+    },
+    stopMessage(index) {
+      if (this.streamController[index]) {
+        this.streamController[index].abort()
+        this.streamController[index] = null
+      }
+      this.chatLoading[index] = false
+      this.messagesloading[index] = false
+    }
   },
   mounted() {
     window.addEventListener('keydown', (e)=>{

+ 56 - 6
src/components/pages/workPage/components/setQuestionsAndAnswers.vue

@@ -61,17 +61,17 @@
       <div class="explanation">
         <div class="e_header">
           <span>{{ lang.ssEvaluationStandard }}</span>
-          <!-- <div v-show="false">
+          <div @click="generateExplanation()" :disabled="chatLoading" v-loading="messagesloading">
             <svg t="1774233113566" viewBox="0 0 1027 1024" width="200" height="200">
               <path
-                d="M511.979521 544.586217a54.525819 54.525819 0 0 1-23.359066-5.247791L31.038758 321.427143a54.397824 54.397824 0 0 1 0-98.236071L488.620455 5.279789c14.719411-7.039718 31.99872-7.039718 46.718131 0l457.581697 217.911283a54.397824 54.397824 0 0 1 0 98.236071l-457.581697 217.911283A54.525819 54.525819 0 0 1 511.979521 544.586217zM180.984761 272.341106L511.979521 429.966801l331.058757-157.625695L511.979521 114.587417 180.984761 272.341106z m354.417823 746.402144l457.517699-217.911283a54.397824 54.397824 0 1 0-46.718131-98.236071L511.979521 909.371625l-434.158634-206.775729a54.397824 54.397824 0 0 0-46.718131 98.236071l457.581697 217.911283a54.525819 54.525819 0 0 0 46.718131 0z m0-239.73441l457.517699-217.847286a54.397824 54.397824 0 1 0-46.718131-98.300068L511.979521 669.573217l-434.158634-206.711731a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z"
+                d="M511.979521 544.586217a54.525819 54.525819 0 0 1-23.359066-5.247791L31.038758 321.427143a54.397824 54.397824 0 0 1 0-98.236071L488.620455 5.279789c14.719411-7.039718 31.99872-7.039718 46.718131 0l457.581697 217.911283a54.397824 54.397824 0 0 1 0 98.236071l-457.581697 217.911283A54.525819 54.525819 0 0 1 511.979521 544.586217zM180.984761 272.341106L511.979521 429.966801l331.058757-157.625695L511.979521 114.587417 180.984761 272.341106z m354.417823 746.402144l457.517699-217.911283a54.397824 54.397824 0 1 0-46.718131-98.236071L511.979521 909.371625l-434.158634-206.775729a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z m0-239.73441l457.517699-217.847286a54.397824 54.397824 0 1 0-46.718131-98.300068L511.979521 669.573217l-434.158634-206.711731a54.397824 54.397824 0 0 0-46.718131 98.300068l457.581697 217.847286a54.525819 54.525819 0 0 0 46.718131 0z"
                 p-id="9073"></path>
             </svg>
-            <span>AI生成</span>
-          </div> -->
+            <span>{{ chatLoading ? lang.ssGenerating : lang.ssAIGenerate }}</span>
+          </div>
         </div>
         <el-input type="textarea" rows="3" resize="none" v-model="jsonData.evaluationCriteria" :placeholder="lang.ssEnterEvaluationStandard"
-          @input="setTestJson()"></el-input>
+          @input="setTestJson()" v-loading="messagesloading" :disabled="chatLoading"></el-input>
       </div>
     </div>
 
@@ -80,7 +80,9 @@
 
 <script>
 import { uploadOneFile } from '@/components/tools/uploadFile'
+import { myMixin } from '@/mixins/mixin'
 export default {
+  mixins: [myMixin],
   emits: ['setTestJson', 'save'],
   props: {
     workData: {
@@ -90,7 +92,11 @@ export default {
   },
   data() {
     return {
-      jsonData: {}
+      jsonData: {},
+      streamController: [],
+      chatLoading: false,
+      messagesloading: false,
+      userid: this.$route.query.userid,
     }
   },
   watch: {
@@ -158,6 +164,50 @@ export default {
       // }
       this.setTestJson();
     },
+    // 生成解释
+    async generateExplanation() {
+      if (this.chatLoading) {
+        this.$message.warning(this.lang.ssPleaseWait);
+        return
+      }
+      if (this.messagesloading) {
+        this.$message.warning(this.lang.ssPleaseWait);
+        return
+      }
+      let prompt = `- 题干:${this.jsonData.answerQ}`
+      this.chatLoading = true
+      this.messagesloading = true
+      this.chat_stream(prompt, '5edc10d5-8635-4fe6-9670-8b2d9e8005aa', this.userid || this.workData.userid, this.lang.lang, (event) => {
+        if (event.type === 'message') {
+          this.jsonData.evaluationCriteria = event.data
+          this.messagesloading = false
+        }
+        else if (event.type === 'messageEnd') {
+          this.jsonData.evaluationCriteria = event.data
+          this.messagesloading = false
+          this.chatLoading = false
+        }else if (event.type === 'error') {
+          this.chatLoading = false
+          this.messagesloading = false
+          this.$message.error(event.data)
+        }
+      }).then(controller => {
+        this.streamController = controller
+      }).catch(err => {
+        this.chatLoading = false
+        this.messagesloading = false
+        console.log('err', err)
+        this.stopMessage()
+      })
+    },
+    stopMessage() {
+      if (this.streamController) {
+        this.streamController.abort()
+        this.streamController = null
+      }
+      this.chatLoading = false
+      this.messagesloading = false
+    }
   },
 }
 </script>

+ 3 - 1
src/lang/cn.json

@@ -2047,6 +2047,7 @@
   "ssConfirmDeleteClassStudentone":"确认移除*学生?",
   "ssExplanationTip":"解释说明",
   "ssAIGenerate":"AI生成",
+  "ssGenerating":"生成中...",
   "ssEnterExplanation":"请输入解释说明",
   "ssEvaluationStandard":"评价标准",
   "ssEnterEvaluationStandard":"请输入评价标准",
@@ -2062,5 +2063,6 @@
   "ssUnconfirmedToolPage": "存在未确认的工具页",
   "ssUnconfirmedToolPageTip": "存在未确认的工具页,请完成后发布课程。",
   "ssIgnoreAndPublish": "忽略并发布",
-  "ssReturnModify": "返回修改"
+  "ssReturnModify": "返回修改",
+  "ssPleaseWait": "请稍后再试"
 }

+ 3 - 1
src/lang/en.json

@@ -2046,6 +2046,7 @@
   "ssConfirmDeleteClassStudentone":"Confirm remove * student?",
   "ssExplanationTip":"Explanation",
   "ssAIGenerate":"AI Generate",
+  "ssGenerating":"Generating...",
   "ssEnterExplanation":"Please enter explanation",
   "ssEvaluationStandard":"Evaluation Standard",
   "ssEnterEvaluationStandard":"Please enter evaluation standard",
@@ -2061,5 +2062,6 @@
   "ssUnconfirmedToolPage": "Unconfirmed Tool Page",
   "ssUnconfirmedToolPageTip": "Unconfirmed tool page, please publish the course after completion.",
   "ssIgnoreAndPublish": "Ignore and publish",
-  "ssReturnModify": "Return to modify"
+  "ssReturnModify": "Return to modify",
+  "ssPleaseWait": "Please wait, try again later."
 }

+ 3 - 1
src/lang/hk.json

@@ -2046,6 +2046,7 @@
   "ssConfirmDeleteClassStudentone":"確定移除 * 名學生嗎?",
   "ssExplanationTip":"解釋說明",
   "ssAIGenerate":"AI生成",
+  "ssGenerating":"生成中...",
   "ssEnterExplanation":"請輸入解釋說明",
   "ssEvaluationStandard":"評價標準",
   "ssEnterEvaluationStandard":"請輸入評價標準",
@@ -2062,5 +2063,6 @@
   "ssUnconfirmedToolPage": "存在未確認的工具頁", 
   "ssUnconfirmedToolPageTip": "存在未確認的工具頁,請完成後發布課程。",
   "ssIgnoreAndPublish": "忽略並發布",
-  "ssReturnModify": "返回修改"
+  "ssReturnModify": "返回修改",
+  "ssPleaseWait": "請稍後再試"
 }

+ 91 - 1
src/mixins/mixin.js

@@ -1,10 +1,13 @@
 import { v4 as uuidv4 } from "uuid";
+import { fetchEventSource } from "@microsoft/fetch-event-source";
+
 
 export const myMixin = {
   data() {
     return {
         userJson: {},
-        new_url: 'https://appapi.cocorobo.cn'
+        new_url: 'https://appapi.cocorobo.cn',
+        model: {}
     };
   },
   methods: {
@@ -134,5 +137,92 @@ export const myMixin = {
           });
       })
     },
+    async getAgentModel(agentId) {
+      if (this.model[agentId]) {
+        return this.model[agentId]
+      }
+      const res = await this.ajax.get('https://appapi.cocorobo.cn/api/agents/agent/' + agentId)
+      this.model[agentId] = res
+      return this.model[agentId]
+    },
+    getTtsLanguage(language) {
+      if (language === 'com') {
+        return 'en-US'
+      } else if (language === 'hk') {
+        return 'zh-HK'
+      } else {
+        return 'zh-CN'
+      }
+    },
+    chat_stream(msg, agentId, userId, language, onMessage, session_name, file_ids, modelName) {
+      const self = this
+      return new Promise(function(resolve) {
+        self.getAgentModel(agentId).then(function(agentData) {
+          var params = {
+            id: agentId,
+            file_ids: file_ids || [],
+            message: 'Language: ' + (language === 'com' ? 'English' : (language === 'hk' ? 'Traditional Chinese' : 'Chinese')) + ' ' + msg + '  ' + (language === 'hk' ? '請用繁體中文回复' : (language === 'com' ? 'Please reply in English' : '请用中文回复')),
+            uid: uuidv4(),
+            stream: true,
+            model: modelName || (agentData && agentData.modelType) || 'open-doubao',
+            userId: userId,
+            tts_language: self.getTtsLanguage(language),
+            session_name: session_name || uuidv4()
+          }
+
+          var ctrl = new AbortController()
+          var content = ''
+          
+          fetchEventSource('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', {
+            method: 'POST',
+            body: JSON.stringify(params),
+            signal: ctrl.signal,
+            headers: {
+              'Content-Type': 'application/json',
+            },
+            onmessage: function(event) {
+              var data = JSON.parse(event.data)
+              if (data.content) {
+                if (data.content != '[DONE]') {
+                  content += data.content
+                  onMessage({
+                    type: 'message',
+                    data: content
+                  })
+                } else {
+                  onMessage({
+                    type: 'messageEnd',
+                    data: content
+                  })
+                }
+              }
+            },
+            onclose: function() {
+              onMessage({
+                type: 'close',
+                data: 'SSE Connection closed'
+              })
+            },
+            onerror: function(err) {
+              onMessage({
+                type: 'error',
+                data: err.message || 'Unknown error'
+              })
+              throw err
+            },
+          }).catch(function(err) {
+            onMessage({
+              type: 'error',
+              data: err || 'Unknown error'
+            })
+            console.log('err', err)
+          })
+          
+          resolve({
+            abort: function() { ctrl.abort() }
+          })
+        })
+      })
+    }
   }
 };