yonge 1 month ago
parent
commit
809da6e438

+ 1 - 1
dist/assets/index-VYVp_B8-.js → dist/assets/index-Bup-yDxT.js

@@ -1 +1 @@
-import{u as c,ag as l}from"./index-CgoX14-6.js";import{w as p,bA as d,a5 as g,cD as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-DCMcs6Y_.js";import"./shapes-DN4jr45j.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};
+import{u as c,ag as l}from"./index-C6td7eq8.js";import{w as p,bA as d,a5 as g,cD as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-DCWQzVyk.js";import"./shapes-B0c-921d.js";const S=()=>{function o(a){if(window.parent===window)return;const{api:e,playState:n}=a.data||{};e==="headerTogge"?window.parent.postMessage({api:"headerTogge",playState:n},"*"):e==="changeTogge"?window.parent.postMessage({api:"changeTogge"},"*"):["clickTempo","clickViewFigner"].includes(e)&&window.parent.postMessage({api:e},"*")}p(()=>{window.addEventListener("message",o)}),d(()=>{window.removeEventListener("message",o)})},_=g({__name:"mobileScreen",setup(o){S();const a=m(),e=t(!0),n=t(!1),i=c();return a.setScreenMode("mobileScreen"),i.initPPTData().then(()=>{e.value=!1}),(v,s)=>(r(),u("div",{class:"mobileScreen",onClick:s[0]||(s[0]=b=>n.value=!1)},[e.value?w("",!0):(r(),f(l,{key:0}))]))}}),P=k(_,[["__scopeId","data-v-90b2692a"]]);export{P as default};

File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-C23V3pxP.css


+ 1 - 1
dist/assets/index-CuHZpN0g.js → dist/assets/index-C2ik0Ucq.js

@@ -1 +1 @@
-import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bP as _,bQ as d,ak as s,bH as p}from"./index-DCMcs6Y_.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};
+import{_ as a}from"./404-R0y5dkvZ.js";import{a5 as o,eo as t,ae as r,af as c,bP as _,bQ as d,ak as s,bH as p}from"./index-DCWQzVyk.js";const i=e=>(_("data-v-dda25179"),e=e(),d(),e),n={class:"errorPage"},m=i(()=>s("div",{class:"error"},[s("img",{src:a,class:"img",alt:""}),s("div",{class:"tit"},"页面找不到了~")],-1)),l=[m],f=o({__name:"errorPage",setup(e){return t(),(u,h)=>(r(),c("div",n,l))}}),I=p(f,[["__scopeId","data-v-dda25179"]]);export{I as default};

+ 1 - 1
dist/assets/index-CgoX14-6.js → dist/assets/index-C6td7eq8.js

@@ -1,4 +1,4 @@
-import{cH as pw,cI as gw,aG as mw,cJ as yw,cK as Sw,cL as bw,cM as xw,ah as Wt,cN as Cw,cO as ww,cP as Tw,cQ as Iw,aM as _w,aF as Aw,cR as Ew,aB as d1,bg as Pw,cS as Mw,cT as Rw,cU as Dw,cV as Lw,cW as kw,cX as Ow,cY as Bw,aE as Nw,cZ as Fw,s as Te,bl as $w,al as je,ag as Fe,af as he,ak as Z,c_ as zw,c$ as Uw,d0 as Hw,d1 as Vw,bk as Gw,cy as cl,aS as wv,aq as Ye,d2 as Ww,d3 as jw,a5 as ze,d4 as Kw,d5 as Xw,d6 as Yw,d7 as Zw,d8 as Qw,d9 as Jw,da as qw,db as eT,dc as tT,dd as rT,de as nT,v as iT,df as aT,dg as oT,dh as sT,aW as lT,di as uT,dj as cT,dk as fT,dl as dT,dm as hT,ay as ur,dn as vT,dp as pT,dq as gT,dr as mT,A as yT,ds as ST,dt as bT,bq as xT,du as CT,dv as wT,dw as TT,ap as IT,ac as Tv,ai as ct,dx as _T,au as xe,dy as AT,X as h1,y as ET,dz as PT,aP as MT,dA as RT,w as Er,dB as DT,dC as LT,H as kT,dD as OT,bA as hn,aD as BT,ae as ne,bQ as ea,$ as Tc,dE as NT,bP as ta,dF as FT,aC as $T,aO as zT,R as fe,dG as UT,br as ih,b3 as _r,aj as Hs,b1 as lt,b2 as Ua,an as Iv,dH as HT,dI as VT,dJ as GT,dK as WT,dL as jT,dM as KT,dN as XT,Q as YT,dO as ZT,dP as QT,dQ as JT,at as Mt,dR as qT,aR as eI,aY as tI,ad as fl,aZ as rI,dS as nI,dT as iI,dU as aI,P as le,a6 as oI,dV as sI,dW as lI,dX as uI,dY as cI,a7 as fI,dZ as dI,bN as hI,d_ as vI,d$ as pI,bM as gI,b4 as v1,aA as _v,e0 as mI,e1 as yI,E as Jt,x as SI,e2 as bI,e3 as xI,e4 as CI,am as _n,e5 as wI,az as sr,aU as Vs,e6 as TI,as as pr,e7 as II,e8 as Av,cG as yr,by as ah,bI as oh,bJ as _I,bH as Ve,bu as sh,bv as Ev,bx as jt,bB as Ic,bR as di,e9 as Tu,bE as hi,ea as AI,bK as ra,bC as EI,bw as Lt,eb as PI,ec as ui,bO as MI,ed as RI,ee as DI,ef as LI,eg as kI,eh as Jp,ei as OI,bF as qp,cD as _c,cC as BI,ej as Vu,ek as _l}from"./index-DCMcs6Y_.js";/**
+import{cH as pw,cI as gw,aG as mw,cJ as yw,cK as Sw,cL as bw,cM as xw,ah as Wt,cN as Cw,cO as ww,cP as Tw,cQ as Iw,aM as _w,aF as Aw,cR as Ew,aB as d1,bg as Pw,cS as Mw,cT as Rw,cU as Dw,cV as Lw,cW as kw,cX as Ow,cY as Bw,aE as Nw,cZ as Fw,s as Te,bl as $w,al as je,ag as Fe,af as he,ak as Z,c_ as zw,c$ as Uw,d0 as Hw,d1 as Vw,bk as Gw,cy as cl,aS as wv,aq as Ye,d2 as Ww,d3 as jw,a5 as ze,d4 as Kw,d5 as Xw,d6 as Yw,d7 as Zw,d8 as Qw,d9 as Jw,da as qw,db as eT,dc as tT,dd as rT,de as nT,v as iT,df as aT,dg as oT,dh as sT,aW as lT,di as uT,dj as cT,dk as fT,dl as dT,dm as hT,ay as ur,dn as vT,dp as pT,dq as gT,dr as mT,A as yT,ds as ST,dt as bT,bq as xT,du as CT,dv as wT,dw as TT,ap as IT,ac as Tv,ai as ct,dx as _T,au as xe,dy as AT,X as h1,y as ET,dz as PT,aP as MT,dA as RT,w as Er,dB as DT,dC as LT,H as kT,dD as OT,bA as hn,aD as BT,ae as ne,bQ as ea,$ as Tc,dE as NT,bP as ta,dF as FT,aC as $T,aO as zT,R as fe,dG as UT,br as ih,b3 as _r,aj as Hs,b1 as lt,b2 as Ua,an as Iv,dH as HT,dI as VT,dJ as GT,dK as WT,dL as jT,dM as KT,dN as XT,Q as YT,dO as ZT,dP as QT,dQ as JT,at as Mt,dR as qT,aR as eI,aY as tI,ad as fl,aZ as rI,dS as nI,dT as iI,dU as aI,P as le,a6 as oI,dV as sI,dW as lI,dX as uI,dY as cI,a7 as fI,dZ as dI,bN as hI,d_ as vI,d$ as pI,bM as gI,b4 as v1,aA as _v,e0 as mI,e1 as yI,E as Jt,x as SI,e2 as bI,e3 as xI,e4 as CI,am as _n,e5 as wI,az as sr,aU as Vs,e6 as TI,as as pr,e7 as II,e8 as Av,cG as yr,by as ah,bI as oh,bJ as _I,bH as Ve,bu as sh,bv as Ev,bx as jt,bB as Ic,bR as di,e9 as Tu,bE as hi,ea as AI,bK as ra,bC as EI,bw as Lt,eb as PI,ec as ui,bO as MI,ed as RI,ee as DI,ef as LI,eg as kI,eh as Jp,ei as OI,bF as qp,cD as _c,cC as BI,ej as Vu,ek as _l}from"./index-DCWQzVyk.js";/**
 * vue v3.4.34
 * (c) 2018-present Yuxi (Evan) You and Vue contributors
 * @license MIT

+ 1 - 1
dist/assets/index-DUGyxAXa.js → dist/assets/index-CCiYMC4d.js

@@ -1 +1 @@
-import{u as s,ag as r}from"./index-CgoX14-6.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-DCMcs6Y_.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),g=l(d,[["__scopeId","data-v-678905e9"]]);export{g as default};
+import{u as s,ag as r}from"./index-C6td7eq8.js";import{a5 as c,cD as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-DCWQzVyk.js";const m={class:"pptScreen"},d=c({__name:"pptScreen",setup(f){const a=n(),e=p(!0),o=s();return a.setScreenMode("pptScreen"),o.initPPTData().then(()=>{e.value=!1}),(S,k)=>(t(),_("div",m,[e.value?i("",!0):(t(),u(r,{key:0}))]))}}),g=l(d,[["__scopeId","data-v-678905e9"]]);export{g as default};

File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-CHmIuqCA.js


File diff suppressed because it is too large
+ 1 - 1
dist/assets/index-DCWQzVyk.js


+ 1 - 1
dist/assets/index-BcPIKJfq.js → dist/assets/index-DZgPMWmg.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-BB0o1k4b.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bP as m,bQ as f,bH as u}from"./index-DCMcs6Y_.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};
+import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-DhcPcWc_.js";import{a5 as n,ae as i,af as r,ak as o,aq as _,am as d,P as l,aS as p,bP as m,bQ as f,bH as u}from"./index-DCWQzVyk.js";const s=a=>(m("data-v-340bbdaa"),a=a(),f(),a),h={class:"login"},b={class:"error"},g=s(()=>o("img",{src:t,class:"img",alt:""},null,-1)),k=s(()=>o("div",{class:"tit"},"登录已过期或服务器错误!",-1)),v=n({__name:"login",setup(a){function e(){window.close()}return(x,B)=>(i(),r("div",h,[o("div",b,[g,k,_(l(c),{class:"backBtn",type:"primary",plain:"",onClick:e},{default:d(()=>[p("关闭页面")]),_:1})])]))}}),C=u(v,[["__scopeId","data-v-340bbdaa"]]);export{C as default};

File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-DhcPcWc_.js


File diff suppressed because it is too large
+ 0 - 0
dist/assets/index-f5J4qxEG.js


+ 1 - 1
dist/assets/shapes-DN4jr45j.js → dist/assets/shapes-B0c-921d.js

@@ -1,4 +1,4 @@
-import{cG as k,bB as $1}from"./index-DCMcs6Y_.js";import{ah as Z,ai as H1,aj as U,ak as n0,al as c0,am as i0,an as j1,ao as K1,ap as Y1,aq as R}from"./index-CgoX14-6.js";var z1={exports:{}};/*!
+import{cG as k,bB as $1}from"./index-DCWQzVyk.js";import{ah as Z,ai as H1,aj as U,ak as n0,al as c0,am as i0,an as j1,ao as K1,ap as Y1,aq as R}from"./index-C6td7eq8.js";var z1={exports:{}};/*!
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  *

+ 1 - 1
dist/index.html

@@ -10,7 +10,7 @@
     <meta name="description" content="ppt编辑器" />
     <meta name="keywords" content="ppt,powerpoint,office powerpoint,在线ppt,幻灯片,演示文稿,ppt在线制作,Vue3,TypeScript" />
     <title>PPT</title>
-    <script type="module" crossorigin src="./assets/index-DCMcs6Y_.js"></script>
+    <script type="module" crossorigin src="./assets/index-DCWQzVyk.js"></script>
     <link rel="stylesheet" crossorigin href="./assets/index-AavUEaYy.css">
   </head>
   <body>

+ 12 - 1
scripts/aiPptDialogTest.html

@@ -24,6 +24,7 @@ import "/src/assets/styles/prosemirror.scss"
 const scenario = new URLSearchParams(location.search).get("scenario") || "normal"
 setToken(`local-ai-ppt-regression-${scenario}`)
 const generated = ref(0)
+const reportedIssues = ref([])
 const outlineCalls = ref(0), savedCalls = ref(0), submitted = ref(null)
 const savedText = ref("")
 let pollCount = 0
@@ -91,6 +92,14 @@ httpAxios.axioseRquest = async config => {
       data.reviewMessage="声音强弱的练习与评价尚未完整关联"
     }
     if (scenario === 'whole-repair-draft') data.reviewMessage = '第2页:学生画面缺少具体范例;整课一致性复核暂未完成'
+    if (scenario === 'completion-advice') {
+      data.reviewMessage = '第2页:建议增加同伴交流;建议:先讨论再举手'
+      data.blockingMessage = ''
+    }
+    if (scenario === 'completion-blocked') {
+      data.reviewMessage = '第2页:建议增加同伴交流'
+      data.blockingMessage = '第5页:故事内容未完成,请提供本页可讲述的storyScript'
+    }
     if (scenario === 'layout-overflow' && !repaired) {
       data.outline.pages[1].visualPlan = { layout: 'text_focus', composition: 'steps', visualMode: 'text_only', textPresentation: 'steps' }
       data.outline.pages[1].studentContent = { headline: '内容过密回归', points: Array.from({ length: 14 }, (_, i) => `第${i + 1}条:` + '保持完整的教材内容,不能用截字方式通过检查。'.repeat(5)) }
@@ -120,6 +129,7 @@ httpAxios.axioseRquest = async config => {
     if (scenario.includes("network") && pollCount === 1) throw new Error("测试网络中断")
     data={taskId:config.url.split("/").pop(),status:scenario.includes("failure") && generated.value === 1 ? "FAILED" : scenario.includes("slow") && pollCount < 6 ? "RUNNING" : scenario.includes("draft") ? "DRAFT" : "DONE",progress:pollCount < 6 && scenario.includes("slow") ? 50 : 100,message:scenario.includes("failure") ? "测试:生成服务暂不可用" : "正在完善课堂任务"}
     if (scenario === 'image-completion' && !repaired) Object.assign(data, { status: 'DRAFT', progress: 95, message: '第2页配图尚未完成;已保留生成结果,可继续完成课件' })
+    if (scenario === 'completion-blocked') Object.assign(data, { status: 'DRAFT', progress: 95, message: '课堂内容尚未完成修正' })
   }
   else throw new Error("测试禁止未声明的业务请求")
   return {data:{code:200,data}}
@@ -188,9 +198,10 @@ const app=createApp({setup(){
     runtimeError.value ? h("pre", runtimeError.value) : null,
     savedCalls.value ? h("p",{id:"saved-text"},`保存内容:${savedText.value}`):null,
     h("p",{id:"test-status"},`大纲请求:${outlineCalls.value};生成请求次数:${generated.value};完善请求:${repairs.value};保存请求:${savedCalls.value};历史失败任务查询:${historicalPolls.value};已应用页数:${slidesStore.slides.length};资源检查:${resourceCheck.value}`),
+    h('p', {id:'review-status'}, `复核意见:${reportedIssues.value.filter(issue => issue.code === 'teaching-review').map(issue => issue.severity).join('、')}`),
     submitted.value ? h("p",{id:"submitted-plan"},`模式:${submitted.value.outlineMode};提交的摘要:${submitted.value.outline.pages.map(p=>p.teachingBrief).join("、")};旧正文数量:${submitted.value.outline.pages.filter(p=>p.studentContent?.points?.length || p.bullets?.length).length};页数:${submitted.value.outline.pages.length}`):null,
     scenario.startsWith("editor") ? h("div",{style:"height:calc(100vh - 65px)"},[h(useScreenStore().screening ? Screen : Editor)]) : h("button",{onClick:()=>{visible.value=true}},"打开AI课件"),
-    scenario.startsWith("editor") ? null : h(AiDialog,{visible:visible.value,"onUpdate:visible":value=>{visible.value=value}}),
+    scenario.startsWith("editor") ? null : h(AiDialog,{visible:visible.value,"onUpdate:visible":value=>{visible.value=value},onGenerated:issues=>{reportedIssues.value=issues}}),
     h("button",{onClick:()=>useSnapshotStore().unDo()},"测试撤销")
   ])
 }})

+ 7 - 1
scripts/aiPptOutlineTest.mjs

@@ -6,9 +6,15 @@ try {
   const { editableOutlinePages, confirmedCoursewareOutline, inspectConfirmedOutline } = await vite.ssrLoadModule('/src/utils/aiCoursewareOutline.ts')
   const { aiCoursewareToSlides } = await vite.ssrLoadModule('/src/utils/aiCoursewareToSlides.ts')
   const { aiCoursewareHttpError } = await vite.ssrLoadModule('/src/utils/aiCoursewareHttpError.ts')
-  const { teachingReviewIssues, blockingImageIssues } = await vite.ssrLoadModule('/src/utils/aiPptFeedback.ts')
+  const { teachingReviewIssues, blockingImageIssues, coursewareCompletionMessage } = await vite.ssrLoadModule('/src/utils/aiPptFeedback.ts')
   assert.equal(teachingReviewIssues('整课一致性复核暂未完成,请核对跨页的角色、动作、乐器与练习安排')[0].severity, 'warning')
   assert.equal(teachingReviewIssues('第2页:缺少听辨活动')[0].severity, 'error')
+  assert.equal(teachingReviewIssues('第2页:建议增加同伴交流', '')[0].severity, 'warning')
+  const review = teachingReviewIssues('第2页:建议增加同伴交流', '第3页:故事内容未完成')
+  assert.deepEqual(review.map(issue => [issue.pageNo, issue.severity]), [[3, 'error'], [2, 'warning']])
+  assert.match(coursewareCompletionMessage({blockingMessage:'第3页:缺少storyScript',reviewMessage:'第2页:建议交流'}, '课堂内容尚未完成修正'), /第3页:缺少故事讲稿/)
+  assert.match(coursewareCompletionMessage({reviewMessage:'整课一致性复核暂未完成'}, '第2页配图尚未完成;已保留生成结果'), /第2页配图尚未完成/)
+  assert.match(coursewareCompletionMessage({reviewMessage:'第2页:缺少studentContent.example'}, '课堂内容尚未完成修正'), /第2页:缺少学生可见范例/)
   assert.equal(blockingImageIssues([{code:'missing_required_visual'}, {code:'resource_unavailable',message:'图片无法加载'}, {code:'missing_page_audio'}, {code:'missing_score'}]).length, 2)
   assert.match(aiCoursewareHttpError({ response: { status: 504, data: {} } }).message, /超时/)
   assert.match(aiCoursewareHttpError({ code: 'ECONNABORTED' }).message, /超时/)

+ 17 - 0
scripts/aiPptSelfTest.mjs

@@ -331,6 +331,23 @@ try {
   const noObjectives = structuredClone(valid)
   noObjectives.pages.forEach(page => page.objectiveIds = [])
   assert(codes(noObjectives).includes("objective_coverage"))
+  const cultureResponse = structuredClone(valid)
+  cultureResponse.textbookResearch.teachingObjectives = ['初步了解伶伦制律的文化背景']
+  cultureResponse.pages.forEach(page => { page.objectiveIds = [] })
+  const culturePage = cultureResponse.pages[2]
+  culturePage.type = 'work_context'; culturePage.objectiveIds = ['1']; culturePage.optional = false
+  culturePage.classroomAction = { teacherInstruction: '讲述故事后请学生回答。',
+    studentTask: '听完故事,举手回答:伶伦是怎样发现音律的?',
+    modelExample: '模仿凤凰叫声,再请学生回答伶伦听到了什么。', successCriteria: '学生能说出“听凤凰叫声”。' }
+  assert(!codes(cultureResponse).includes('objective_coverage'), '有具体口头作答和判断标准的背景页应计入目标评价')
+  culturePage.classroomAction.successCriteria = '学生认真听讲。'
+  assert(codes(cultureResponse).includes('objective_coverage'), '单纯听讲不能冒充目标评价')
+  const { modelPageContent: responsePageContent } = await vite.ssrLoadModule('/src/utils/aiPptContent.ts')
+  const sameInstruction = { ...culturePage, type:'listen_task', title:'声音捉迷藏',
+    studentContent:{ headline:'听到哪种声音,就伸出几个手指?', points:['1 — 风声','2 — 水声','3 — 鸟鸣'] },
+    classroomAction:{ ...culturePage.classroomAction, studentTask:'听到哪种声音,就伸出几个手指?' } }
+  assert.equal(responsePageContent(sameInstruction).task, '', '相同引导语与任务在学生画面只显示一次')
+  assert.equal(responsePageContent(sameInstruction).heading, sameInstruction.studentContent.headline)
   const withVideo = structuredClone(valid)
   withVideo.pages[7].resourceBindings = [{resourceType:"VIDEO",resourceId:"test-video",contentUrl:"https://example.com/test.mp4",verified:true}]
   assert(render(withVideo)[7].elements.some(element => element.subtype === "elf-video"))

+ 1 - 0
src/api/aiCourseware.ts

@@ -210,6 +210,7 @@ export interface AiCoursewareTaskInfo {
 
 export interface AiCoursewareGenerateResult {
   reviewMessage?: string
+  blockingMessage?: string
   context: Record<string, any>
   outline: AiCoursewareOutline
 }

+ 3 - 3
src/hooks/useAiCoursewarePageRepair.ts

@@ -11,7 +11,7 @@ import { getAiPptImageAspectRatios } from "@/utils/aiPptTemplateEngine"
 import { DEFAULT_AI_COURSEWARE_TEMPLATE_ID } from "@/config/aiCoursewareTemplates"
 import { inspectAiPptQuality, blockingAiPptLayoutIssues, type AiPptQualityIssue } from "@/utils/aiPptQuality"
 import { inspectAiPptResources } from "@/utils/aiPptResources"
-import { teachingReviewIssues, blockingImageIssues } from '@/utils/aiPptFeedback'
+import { teachingReviewIssues, blockingImageIssues, coursewareCompletionMessage } from '@/utils/aiPptFeedback'
 
 const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value))
 
@@ -91,7 +91,7 @@ export default function useAiCoursewarePageRepair() {
       if (draft) {
         // 保留服务端已经完成的配图供下次续作,画布保持不变。
         outline = nextOutline
-        throw new Error('课件尚未全部完成,已保留生成内容和成功配图,请继续完成。当前页面未被替换。')
+        throw new Error(`${coursewareCompletionMessage(result.data)}当前页面未被替换。`)
       }
       await assertAiPptClientCurrent()
       if (!active()) return
@@ -119,7 +119,7 @@ export default function useAiCoursewarePageRepair() {
       if (whole) baseline = nextSlides.map(slide => JSON.stringify(slide))
       else baseline[pageNo - 1] = JSON.stringify(nextSlides[latest])
       const pageIssues = report.issues.filter(issue => (whole || issue.pageNo === pageNo) && (issue.severity === "error" || ["image_review", "missing_required_visual"].includes(issue.code)))
-      const reviewIssues = teachingReviewIssues(result.data.reviewMessage)
+      const reviewIssues = teachingReviewIssues(result.data.reviewMessage, result.data.blockingMessage)
       if (draft && !reviewIssues.length) reviewIssues.push({ code: 'teaching-review', severity: 'error', message: '课堂内容尚未通过复核,请继续完善草稿。' })
       issues.value = [...(whole ? [] : issues.value.filter(issue => issue.pageNo !== pageNo && issue.code !== 'teaching-review')),
         ...reviewIssues, ...pageIssues, ...resourceIssues.map(issue => whole ? issue : { ...issue, pageNo })]

+ 17 - 6
src/utils/aiPptFeedback.ts

@@ -1,4 +1,4 @@
-import type { AiCoursewarePage } from '@/api/aiCourseware'
+import type { AiCoursewareGenerateResult, AiCoursewarePage } from '@/api/aiCourseware'
 import type { AiPptQualityIssue } from '@/utils/aiPptQuality'
 
 /** 教师只需知道待补内容和处理方法,审核原文保留在生成结果中供开发诊断。 */
@@ -18,7 +18,7 @@ export function teacherReviewMessage(message: string) {
     'studentContent.steps': '学生操作步骤', 'studentContent.points': '学生画面中的示例',
     'studentContent.example': '学生可见范例', 'studentContent': '学生画面内容', 'comparisonGroups': '对比组',
     'classroomAction.modelExample': '具体教学示范', 'classroomAction.successCriteria': '判断标准',
-    'classroomAction.storyScript': '可讲述的故事', 'storyScript': '可讲述的故事',
+    'classroomAction.storyScript': '故事讲稿', 'storyScript': '故事讲稿',
     'interaction.explanation': '答案依据', 'interaction.answer': '教师参考答案',
     'visualPlan.composition': '页面构图', 'visualPlan': '页面设计', 'composition': '页面构图',
     'visualMode': '图片或图示形式', 'visualShare': '主图比例', 'textPresentation': '文字排版',
@@ -28,14 +28,25 @@ export function teacherReviewMessage(message: string) {
   return Object.entries(names).reduce((text, [key, value]) => text.split(key).join(value), message)
 }
 
-export function teachingReviewIssues(message?: string): AiPptQualityIssue[] {
-  if (!message?.trim()) return []
-  return [...new Set(message.split(/;(?=第\d+页|课堂|整课|“)/).map(text => text.trim()).filter(Boolean))].map(text => ({
-    code: 'teaching-review', severity: text === '整课一致性复核暂未完成,请核对跨页的角色、动作、乐器与练习安排' ? 'warning' : 'error', pageNo: text.match(/^第(\d+)页/) ? Number(text.match(/^第(\d+)页/)![1]) : undefined,
+export function teachingReviewIssues(message?: string, blockingMessage?: string): AiPptQualityIssue[] {
+  const split = (value?: string) => value?.split(/;(?=第\d+页|课堂|整课|“)/).map(text => text.trim()).filter(Boolean) || []
+  const blocking = split(blockingMessage)
+  return [...new Set([...blocking, ...split(message)])].map(text => ({
+    code: 'teaching-review', severity: text === '整课一致性复核暂未完成,请核对跨页的角色、动作、乐器与练习安排'
+      || (blockingMessage !== undefined && !blocking.includes(text)) ? 'warning' : 'error',
+    pageNo: text.match(/^第(\d+)页/) ? Number(text.match(/^第(\d+)页/)![1]) : undefined,
     message: teacherReviewMessage(text)
   }))
 }
 
+export function coursewareCompletionMessage(result: Pick<AiCoursewareGenerateResult, 'blockingMessage' | 'reviewMessage'>, taskMessage?: string) {
+  const detail = result.blockingMessage?.trim()
+    || (taskMessage?.includes('配图') ? taskMessage.split(';')[0] : result.reviewMessage?.trim())
+  return teacherReviewMessage(detail
+    ? `${detail};已保留生成内容和成功配图,可继续完成课件。`
+    : taskMessage || '课件尚未全部完成,可接着完成。')
+}
+
 export const blockingImageIssues = (issues: AiPptQualityIssue[]) => issues.filter(issue =>
   ['missing_required_visual', 'missing_visual_render'].includes(issue.code)
   || (issue.code === 'resource_unavailable' && issue.message.startsWith('图片')))

+ 7 - 2
src/utils/aiPptQuality.ts

@@ -261,11 +261,16 @@ function inspectTeachingContract(outline: AiCoursewareOutline, slides: Slide[],
   let minutes = 0
   pages.forEach((page, index) => {
     const no = index + 1
+    // 与服务端一致:有具体作答、示范和判断标准的介绍页也可以承载目标评价。
+    const action = page.classroomAction
+    const response = page.type !== 'cover' && !!action?.modelExample?.trim()
+      && /(回答|说出|指出|选出|指认|复述|描述|解释|判断|比较)/.test(action?.studentTask || '')
+      && /(正确|匹配|说出|回答|指出|选出|复述|描述|解释|判断)/.test(action?.successCriteria || '')
     for (const id of page.objectiveIds || []) {
       if (!/^[1-9][0-9]*$/.test(id) || Number(id) > objectives.length) push(issues, "invalid_objective", "error", no, "教学目标编号无效")
       else if (!page.optional) {
-        if (/practice|activity|sing|listen/.test(page.type)) taught.add(id)
-        if (/performance|creative/.test(page.type) || page.type === "practice_check") assessed.add(id)
+        if (response || /practice|activity|sing|listen/.test(page.type)) taught.add(id)
+        if (response || /performance|creative/.test(page.type) || page.type === "practice_check") assessed.add(id)
       }
     }
     const slide = slides[index]

+ 3 - 3
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -228,7 +228,7 @@ import { aiCoursewareToSlides } from "@/utils/aiCoursewareToSlides"
 import { getAiPptImageAspectRatios } from "@/utils/aiPptTemplateEngine"
 import { inspectAiPptQuality, type AiPptQualityIssue } from "@/utils/aiPptQuality"
 import { inspectAiPptResources } from "@/utils/aiPptResources"
-import { teachingReviewIssues, blockingImageIssues } from '@/utils/aiPptFeedback'
+import { teachingReviewIssues, blockingImageIssues, coursewareCompletionMessage } from '@/utils/aiPptFeedback'
 import { templateAssetPath } from "@/utils/templateAssetUrl"
 import { draftKey, loadAiDraft, saveAiDraft } from "@/utils/aiCoursewareDraft"
 import {
@@ -720,7 +720,7 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
   if (!active()) return
   if (task.value?.status === 'DRAFT') {
     completionSource.value = clone(result.outline)
-    generationError.value = task.value.message || '课件尚未全部完成,可接着完成。'
+    generationError.value = coursewareCompletionMessage(result, task.value.message)
     remember()
     return
   }
@@ -746,7 +746,7 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
     ...report.warnings.filter(issue => ["image_review", "missing_required_visual", "layout_variety", "layout_consecutive", "rendered_layout_repeat", "invalid_page_design"].includes(issue.code)),
     ...(await inspectAiPptResources(result.outline, converted.slides))
   ]
-  issues.unshift(...teachingReviewIssues(result.reviewMessage))
+  issues.unshift(...teachingReviewIssues(result.reviewMessage, result.blockingMessage))
   const imageIssues = blockingImageIssues(issues)
   if (imageIssues.length) {
     if (!active()) return

Some files were not shown because too many files changed in this diff