Просмотр исходного кода

feat(student-choice-question): 新增831题型AI教学分析功能

1.  修复多处模板标签换行缩进问题,优化代码格式
2.  新增AI分析面板,支持基于全班KWL表格和学习反思生成教学分析
3.  实现AI分析刷新、结果渲染和更新时间展示逻辑
4.  编写完整的AI分析提示词,规范分析输出格式
lsc 1 неделя назад
Родитель
Сommit
544a02d779
1 измененных файлов с 162 добавлено и 19 удалено
  1. 162 19
      src/views/Student/components/choiceQuestionDetailDialog.vue

+ 162 - 19
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -15,7 +15,7 @@
             <span>{{
               props.showData.choiceQuestionListData[props.showData.workIndex]
                 .teststitle
-              }}</span>
+            }}</span>
           </div>
           <div class="c_t45_msg" v-if="props.roleType == 1">
             <div>{{ lang.ssAnswerCount }} {{ props.workArray.length }}<span
@@ -120,7 +120,7 @@
             <span>{{
               props.showData.choiceQuestionListData[props.showData.workIndex]
                 .teststitle
-              }}</span>
+            }}</span>
           </div>
           <div class="c_t45_msg" v-if="props.roleType == 1">
             <div>{{ lang.ssAnswerCount }} {{ props.workArray.length }}<span
@@ -493,21 +493,15 @@
           </div>
           <div class="c_t79_wd_content">
             <!-- 图片 -->
-            <img
-              v-if="getFileType(lookWorkData.content.fileList[lookWorkIndex].url) === 'image'"
+            <img v-if="getFileType(lookWorkData.content.fileList[lookWorkIndex].url) === 'image'"
               :src="lookWorkData.content.fileList[lookWorkIndex].url"
               @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])" />
             <!-- 视频 -->
-            <video
-              v-else-if="getFileType(lookWorkData.content.fileList[lookWorkIndex].url) === 'video'"
-              :src="lookWorkData.content.fileList[lookWorkIndex].url"
-              controls
+            <video v-else-if="getFileType(lookWorkData.content.fileList[lookWorkIndex].url) === 'video'"
+              :src="lookWorkData.content.fileList[lookWorkIndex].url" controls
               @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])" />
             <!-- 文档/PDF/Office -->
-            <div
-              v-else
-              class="file-preview-card"
-              @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])">
+            <div v-else class="file-preview-card" @click.stop="lookImage(lookWorkData.content.fileList[lookWorkIndex])">
               <div class="file-preview-icon">
                 {{ getFileIcon(getFileType(lookWorkData.content.fileList[lookWorkIndex].url)) }}
               </div>
@@ -776,6 +770,37 @@
           </div>
         </div>
 
+        <div class="aiAnalysis" style="margin-top:1rem ;"
+            v-if="processedWorkArray.length > 0 && lookWorkData === null && props.showData.toolType == '831' && props.roleType == 1"
+            @click.stop="clickContent(false)">
+            <div class="ai_header">
+              <div class="ai_title">
+                <svg viewBox="0 0 1024 1024" width="200" height="200">
+                  <path
+                    d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z">
+                  </path>
+                  <path
+                    d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z">
+                  </path>
+                </svg>{{ lang.ssAnalysis }}
+              </div>
+              <div class="ai_refresh" :class="{ 'disabled': currentAnalysis && currentAnalysis.loading }"
+                @click.stop="aiAnalysisRefresh831()">
+                {{ lang.ssAIGenerate }}
+                <svg viewBox="0 0 1024 1024" width="200" height="200">
+                  <path
+                    d="M875 483c-33.4 0-60.5 27.1-60.5 60.5v0.1C814.4 710.3 678.8 846 512 846S209.5 710.3 209.5 543.5 345.2 241 512 241c36.8 0 71.7 7.6 104.4 19.7-32 3-57.4 29.1-57.4 61.9 0 34.8 28.2 63 63 63h201.9c34.8 0 63-28.2 63-63V120c0-34.8-28.2-63-63-63s-63 28.2-63 63v81.4C691 150.5 605.2 120 512 120 278.1 120 88.5 309.6 88.5 543.5S278.1 967 512 967s423.5-189.6 423.5-423.5c0-33.4-27.1-60.5-60.5-60.5z">
+                  </path>
+                </svg>
+
+              </div>
+            </div>
+            <div class="ai_content" v-if="currentAnalysis && currentAnalysis.json" v-html="currentAnalysis.json.text">
+            </div>
+            <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}
+            </div>
+          </div>
+
         <div class="c_t72_workDetail" v-if="lookWorkData" @click.stop="clickContent(false)">
           <div class="c_t72_wd_top">
             <img src="../../../assets/img/arrow_left.png" @click.stop="lookWork('')" v-if="!isCastScreen" />
@@ -787,6 +812,8 @@
               {{ lang.ssExitCastScreen }}
             </div>
           </div>
+
+
           <div class="c_t831_wd_content">
             <!-- KWL 三栏 -->
             <div class="kwl-grid" v-if="lookWorkData.content">
@@ -828,7 +855,8 @@
                 </div>
                 <div class="kwl-col__body">
                   <template v-if="lookWorkData.content.learned.length">
-                    <div v-for="(item, idx) in lookWorkData.content.learned" :key="'l-' + idx" class="kwl-item">{{ item }}
+                    <div v-for="(item, idx) in lookWorkData.content.learned" :key="'l-' + idx" class="kwl-item">{{ item
+                      }}
                     </div>
                   </template>
                   <div v-else class="kwl-empty">{{ lang.ssNone }}</div>
@@ -849,7 +877,10 @@
               </div>
             </div>
           </div>
+
+
         </div>
+        
       </div>
     </div>
     <previewImageTool ref="previewImageToolRef" />
@@ -876,12 +907,8 @@
             <line x1="6" y1="6" x2="18" y2="18" />
           </svg>
         </div>
-        <iframe
-          v-if="docPreviewType === 'pdf' || docPreviewType === 'office'"
-          :src="getDocPreviewUrl(docPreviewType, docPreviewUrl)"
-          class="file-preview-frame"
-          frameborder="0"
-        ></iframe>
+        <iframe v-if="docPreviewType === 'pdf' || docPreviewType === 'office'"
+          :src="getDocPreviewUrl(docPreviewType, docPreviewUrl)" class="file-preview-frame" frameborder="0"></iframe>
         <div v-else class="file-preview-office">
           <div class="file-preview-office-icon">📎</div>
           <div class="file-preview-office-name">{{ docPreviewUrl.split('/').pop() }}</div>
@@ -2619,6 +2646,122 @@ ${a.content}\n`
   })
 }
 
+// ai应用
+const aiAnalysisRefresh831 = async () => {
+
+  let chatMsg = ``
+  console.log('processedWorkArray.value', processedWorkArray.value)
+  processedWorkArray.value.forEach((i) => {
+    if (typeof i.content === 'object') {
+      const c = i.content
+      const studentName = i.name || ''
+      if (c.know && c.know.length) {
+        chatMsg += `\n${studentName} - K|已知:\n${c.know.join(';')}\n`
+      }
+      if (c.want && c.want.length) {
+        chatMsg += `\n${studentName} - W|想知:\n${c.want.join(';')}\n`
+      }
+      if (c.learned && c.learned.length) {
+        chatMsg += `\n${studentName} - L|已学:\n${c.learned.join(';')}\n`
+      }
+      if (c.reflection) {
+        chatMsg += `\n${studentName} - 学习反思:\n${c.reflection}\n`
+      }
+    }
+  })
+
+  const msg = `# CONTEXT #
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+- 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+- 已提交人数:${props.showData.workArray.length}
+你是一名教学分析助手。请根据全班学生提交的 KWL 表格与学习反思内容,从班级整体视角进行简洁分析,帮助教师在课堂中快速了解学生情况。
+
+请按照以下 4 个维度分析:
+1. K|已知(Know)
+2. W|想知(Want to know)
+3. L|已学(Learned)
+4. 学习反思
+
+分析要求:
+- 每个维度先统计实际有效作答人数及占全班比例。
+- 只分析实际有学生填写的维度;如果某个维度无人填写,则不要输出该维度。
+- 学生未填写某个维度,不代表“不知道”“没学会”或“没有反思”,禁止据此推断。
+- 将意思相近的回答合并归纳,优先呈现班级共性。
+- 如存在明显误解、集中困惑或值得教师关注的问题,可简短指出。
+- 不逐个学生罗列,不评价个人,不排名。
+- 不要为了填满表格而强行生成结论;没有明显发现的内容可以省略。
+- 输出必须简洁,每个单元格尽量控制在 1–2 句话。
+
+请仅使用以下表格格式输出:
+
+| 维度 | 填写情况 | 班级主要情况 | 教师可关注 |
+|---|---|---|---|
+| K|已知 | X人,X% | 概括学生主要已有认知,最多2点 | 如有明显误解或差异则指出,否则写“-” |
+| W|想知 | X人,X% | 概括学生最集中的问题或兴趣,最多2点 | 值得后续回应的问题,最多1点 |
+| L|已学 | X人,X% | 概括学生主要学习收获,最多2点 | 未充分理解或存在混淆的内容,最多1点 |
+| 学习反思 | X人,X% | 概括主要收获、困难或学习感受,最多2点 | 后续教学可调整或跟进的重点,最多1点 |
+
+注意:
+- 无人填写的维度整行删除。
+- 避免空泛表达,例如“学生学习情况较好”“总体表现不错”。
+- 尽量使用具体主题、概念或问题名称。
+
+# 学生提交数据 #
+${chatMsg}`
+
+
+
+
+  console.log('workUrl', props.workUrl)
+  console.log('ai应用提示词', msg)
+  if (!currentAnalysis.value) {
+    aiAnalysisData.value.push({
+      pid: props.workId + (props.cid ? ',' + props.cid : ''),
+      index: props.showData.workIndex,
+      loading: true,
+      json: { text: '', echartsData: '' },
+      noEnd: true,
+      update_at: '',
+      create_at: '',
+    })
+  }
+  else {
+    aiAnalysisData.value.find((item: any) => {
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+    }).loading = true
+    aiAnalysisData.value.find((item: any) => {
+      return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+    }).json = { text: '', echartsData: '' }
+  }
+
+  chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, lang.lang, (event) => {
+    if (event.type === 'message') {
+      aiAnalysisData.value.find((item: any) => {
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).json.text = md.render(event.data)
+
+      aiAnalysisData.value.find((item: any) => {
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).loading = false
+    }
+    else if (event.type === 'messageEnd') {
+      aiAnalysisData.value.find((item: any) => {
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).json.text = md.render(event.data)
+      aiAnalysisData.value.find((item: any) => {
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).noEnd = false
+      aiAnalysisData.value.find((item: any) => {
+        return item.pid === props.workId + (props.cid ? ',' + props.cid : '') && item.index === props.showData.workIndex
+      }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
+      saveAnalysis()
+    }
+  }, '', [], 'open-qwen-plus-latest').catch(err => {
+    console.log('err', err)
+  })
+}
 
 // 当前分析
 const currentAnalysis = computed(() => {