yonge 2 days ago
parent
commit
9ff8a57acd

+ 1 - 1
dist/assets/index-CXoBn-__.js → dist/assets/index-B0ExMQdV.js

@@ -1 +1 @@
-import{u as c,ah as l}from"./index-CnN2CM8W.js";import{w as p,bA as d,a5 as g,cE as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-2h60LrB_.js";import"./shapes-CEo4WaXJ.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}))]))}}),E=k(_,[["__scopeId","data-v-90b2692a"]]);export{E as default};
+import{u as c,ah as l}from"./index-CMb64Y3q.js";import{w as p,bA as d,a5 as g,cE as m,R as t,ae as r,af as u,al as f,ag as w,bH as k}from"./index-D5u1pD2Q.js";import"./shapes-Bj1iCeu-.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}))]))}}),E=k(_,[["__scopeId","data-v-90b2692a"]]);export{E as default};

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


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


+ 1 - 1
dist/assets/index-CnN2CM8W.js → dist/assets/index-CMb64Y3q.js

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

+ 1 - 1
dist/assets/index-BC5zTAN0.js → dist/assets/index-D357kF18.js

@@ -1 +1 @@
-import{_ as t}from"./404-R0y5dkvZ.js";import{E as c}from"./index-DiYfm6-W.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-2h60LrB_.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-xLFyt8_5.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-D5u1pD2Q.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-D3eAuGtV.css


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


+ 1 - 1
dist/assets/index-DzGDNavQ.js → dist/assets/index-DZTO8Ox_.js

@@ -1 +1 @@
-import{u as s,ah as r}from"./index-CnN2CM8W.js";import{a5 as c,cE as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-2h60LrB_.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}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};
+import{u as s,ah as r}from"./index-CMb64Y3q.js";import{a5 as c,cE as n,R as p,ae as t,af as _,al as u,ag as i,bH as l}from"./index-D5u1pD2Q.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}))]))}}),P=l(d,[["__scopeId","data-v-678905e9"]]);export{P as default};

+ 1 - 1
dist/assets/index-D18H1nIO.js → dist/assets/index-Do_XpLna.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-2h60LrB_.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-D5u1pD2Q.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};

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


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


+ 1 - 1
dist/assets/shapes-CEo4WaXJ.js → dist/assets/shapes-Bj1iCeu-.js

@@ -1,4 +1,4 @@
-import{cH as k,bB as $1}from"./index-2h60LrB_.js";import{ai as Z,aj as H1,ak as U,al as n0,am as c0,an as i0,ao as j1,ap as K1,aq as Y1,ar as R}from"./index-CnN2CM8W.js";var z1={exports:{}};/*!
+import{cH as k,bB as $1}from"./index-D5u1pD2Q.js";import{ai as Z,aj as H1,ak as U,al as n0,am as c0,an as i0,ao as j1,ap as K1,aq as Y1,ar as R}from"./index-CMb64Y3q.js";var z1={exports:{}};/*!
  * clipboard.js v2.0.11
  * clipboard.js v2.0.11
  * https://clipboardjs.com/
  * https://clipboardjs.com/
  *
  *

+ 1 - 1
dist/index.html

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

+ 22 - 5
scripts/aiPptDialogTest.html

@@ -13,9 +13,13 @@ import "/src/assets/styles/global.scss"
 import "/src/assets/styles/elementTheme.scss"
 import "/src/assets/styles/elementTheme.scss"
 import "/src/assets/styles/prosemirror.scss"
 import "/src/assets/styles/prosemirror.scss"
 // 所有业务接口使用本地夹具,不调用生成服务,不修改任何线上课件。
 // 所有业务接口使用本地夹具,不调用生成服务,不修改任何线上课件。
-setToken("local-ai-ppt-regression-only")
+const scenario = new URLSearchParams(location.search).get("scenario") || "normal"
+setToken(`local-ai-ppt-regression-${scenario}`)
 const generated = ref(0)
 const generated = ref(0)
+const repairs = ref(0)
+let repaired = false
 const resourceCheck = ref("检查中")
 const resourceCheck = ref("检查中")
+const runtimeError = ref("")
 const types = ["cover", "scene_intro", "objectives", "music_knowledge", "rhythm_practice", "performance_activity", "practice_check", "creative_activity", "summary"]
 const types = ["cover", "scene_intro", "objectives", "music_knowledge", "rhythm_practice", "performance_activity", "practice_check", "creative_activity", "summary"]
 const layouts = ["cover_scene", "hero_focus", "objectives", "split_content", "diagram_focus", "activity", "text_focus", "activity", "summary"]
 const layouts = ["cover_scene", "hero_focus", "objectives", "split_content", "diagram_focus", "activity", "text_focus", "activity", "summary"]
 const outline = {
 const outline = {
@@ -31,9 +35,19 @@ const outline = {
 httpAxios.axioseRquest = async config => {
 httpAxios.axioseRquest = async config => {
   let data
   let data
   if (config.url.endsWith("/outline")) data = structuredClone(outline)
   if (config.url.endsWith("/outline")) data = structuredClone(outline)
-  else if (config.url.endsWith("/generate")) { generated.value++; data={taskId:"local-task",status:"PENDING",progress:0} }
-  else if (config.url.endsWith("/result")) data={outline:structuredClone(outline),context:{resources:[]}}
-  else if (config.url.includes("/task/")) data={taskId:"local-task",status:"DONE",progress:100}
+  else if (config.url.endsWith("/generate")) {
+    generated.value++
+    if (config.data.repairOnly) { repairs.value++; repaired=true }
+    data={taskId:"local-task",status:"PENDING",progress:0}
+  }
+  else if (config.url.endsWith("/result")) {
+    data={outline:structuredClone(outline),context:{resources:[]}}
+    if (scenario === "draft" && !repaired) {
+      data.outline.pages.forEach(page => page.objectiveIds=[])
+      data.reviewMessage="声音强弱的练习与评价尚未完整关联"
+    }
+  }
+  else if (config.url.includes("/task/")) data={taskId:"local-task",status:scenario === "draft" && !repaired ? "DRAFT" : "DONE",progress:100}
   else throw new Error("测试禁止未声明的业务请求")
   else throw new Error("测试禁止未声明的业务请求")
   return {data:{code:200,data}}
   return {data:{code:200,data}}
 }
 }
@@ -42,13 +56,16 @@ await router.push("/?id=local-ai-ppt-regression")
 await router.isReady()
 await router.isReady()
 const app=createApp({setup(){
 const app=createApp({setup(){
   const visible=ref(false)
   const visible=ref(false)
+  const slidesStore=useSlidesStore()
   nextTick(()=>{visible.value=true})
   nextTick(()=>{visible.value=true})
   return ()=>h("main",[
   return ()=>h("main",[
-    h("p",{id:"test-status"},`生成请求次数:${generated.value};资源检查:${resourceCheck.value}`),
+    runtimeError.value ? h("pre", runtimeError.value) : null,
+    h("p",{id:"test-status"},`生成请求次数:${generated.value};完善请求:${repairs.value};已应用页数:${slidesStore.slides.length};资源检查:${resourceCheck.value}`),
     h("button",{onClick:()=>{visible.value=true}},"打开AI课件"),
     h("button",{onClick:()=>{visible.value=true}},"打开AI课件"),
     h(AiDialog,{visible:visible.value,"onUpdate:visible":value=>{visible.value=value}})
     h(AiDialog,{visible:visible.value,"onUpdate:visible":value=>{visible.value=value}})
   ])
   ])
 }})
 }})
+app.config.errorHandler = error => { runtimeError.value=String(error?.stack || error) }
 app.use(store).use(router).mount("#app")
 app.use(store).use(router).mount("#app")
 // 使用浏览器真实图片解码验证损坏资源阻断及谱图等比缩放。
 // 使用浏览器真实图片解码验证损坏资源阻断及谱图等比缩放。
 const svg='data:image/svg+xml;charset=utf-8,'+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="400" height="100"><rect width="400" height="100" fill="white"/></svg>')
 const svg='data:image/svg+xml;charset=utf-8,'+encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="400" height="100"><rect width="400" height="100" fill="white"/></svg>')

+ 3 - 1
src/api/aiCourseware.ts

@@ -16,6 +16,7 @@ export interface AiCoursewareGenerateQuery {
   visualPrompt?: string
   visualPrompt?: string
   preferences?: AiCoursewarePreferences
   preferences?: AiCoursewarePreferences
   regeneratePageNo?: number
   regeneratePageNo?: number
+  repairOnly?: boolean
 }
 }
 
 
 export interface AiCoursewarePreferences {
 export interface AiCoursewarePreferences {
@@ -160,12 +161,13 @@ export interface AiCoursewareResourceBinding {
 
 
 export interface AiCoursewareTaskInfo {
 export interface AiCoursewareTaskInfo {
   taskId: string
   taskId: string
-  status: "PENDING" | "RUNNING" | "DONE" | "FAILED"
+  status: "PENDING" | "RUNNING" | "DONE" | "DRAFT" | "FAILED"
   progress: number
   progress: number
   message: string
   message: string
 }
 }
 
 
 export interface AiCoursewareGenerateResult {
 export interface AiCoursewareGenerateResult {
+  reviewMessage?: string
   context: Record<string, any>
   context: Record<string, any>
   outline: AiCoursewareOutline
   outline: AiCoursewareOutline
 }
 }

+ 1 - 0
src/utils/aiCoursewareDraft.ts

@@ -13,6 +13,7 @@ export interface AiDraftRecord {
   preferences: AiCoursewarePreferences
   preferences: AiCoursewarePreferences
   extraPrompt: string
   extraPrompt: string
   taskId?: string
   taskId?: string
+  reviewMessage?: string
   resources?: AiCoursewareResourceBinding[]
   resources?: AiCoursewareResourceBinding[]
   protectedContent?: Array<[number, ProtectedPageContent]>
   protectedContent?: Array<[number, ProtectedPageContent]>
   backup?: { title: string; theme: SlideTheme; slides: Slide[]; width: number; ratio: number }
   backup?: { title: string; theme: SlideTheme; slides: Slide[]; width: number; ratio: number }

+ 1 - 1
src/utils/aiPptQuality.ts

@@ -293,7 +293,7 @@ function inspectTeachingContract(outline: AiCoursewareOutline, slides: Slide[],
   })
   })
   objectives.forEach((_, index) => {
   objectives.forEach((_, index) => {
     const id = String(index + 1)
     const id = String(index + 1)
-    if (!taught.has(id) || !assessed.has(id)) push(issues, "objective_coverage", "error", undefined, `教学目标${id}缺少必讲的实践或评价任务`)
+    if (!taught.has(id) || !assessed.has(id)) push(issues, "objective_coverage", "error", undefined, `“${objectives[index]}”尚未关联完整的练习与评价,请自动完善或核对教学安排`)
   })
   })
   if (preferences && Math.abs(minutes - preferences.lessonMinutes) > 2) push(issues, "lesson_duration", "error", undefined, `必讲页面合计${minutes}分钟,与${preferences.lessonMinutes}分钟课时不符(允许2分钟机动)`)
   if (preferences && Math.abs(minutes - preferences.lessonMinutes) > 2) push(issues, "lesson_duration", "error", undefined, `必讲页面合计${minutes}分钟,与${preferences.lessonMinutes}分钟课时不符(允许2分钟机动)`)
 }
 }

+ 124 - 68
src/views/Editor/EditorHeader/AiCoursewareDialog.vue

@@ -62,10 +62,6 @@
         </div>
         </div>
 
 
         <div class="extra">
         <div class="extra">
-          <label class="label">本节课范围</label>
-          <TextArea :value="preferences.lessonScope || ''" @update:value="preferences.lessonScope = $event" :rows="2" placeholder="例如:第一课时,重点学习第一段,不安排整曲演唱" :disabled="working" />
-          <label class="label">学生基础与课堂条件</label>
-          <TextArea :value="preferences.learningContext || ''" @update:value="preferences.learningContext = $event" :rows="2" placeholder="例如:一年级,尚不识谱,有打击乐器,需离线播放" :disabled="working" />
           <div class="label">还有什么特别要求?<span>选填</span></div>
           <div class="label">还有什么特别要求?<span>选填</span></div>
           <TextArea v-model:value="extraPrompt" :rows="3" placeholder="例如:多安排节奏游戏,语言适合一年级学生" :disabled="working" />
           <TextArea v-model:value="extraPrompt" :rows="3" placeholder="例如:多安排节奏游戏,语言适合一年级学生" :disabled="working" />
         </div>
         </div>
@@ -74,6 +70,10 @@
           {{ advancedSettingsExpanded ? "收起高级设置" : "高级设置" }}
           {{ advancedSettingsExpanded ? "收起高级设置" : "高级设置" }}
         </button>
         </button>
         <div v-if="advancedSettingsExpanded" class="generation-preferences advanced-settings">
         <div v-if="advancedSettingsExpanded" class="generation-preferences advanced-settings">
+          <label class="label">本节课范围</label>
+          <TextArea :value="preferences.lessonScope || ''" @update:value="preferences.lessonScope = $event" :rows="2" placeholder="例如:第一课时,重点学习第一段" :disabled="working" />
+          <label class="label">学生基础与课堂条件</label>
+          <TextArea :value="preferences.learningContext || ''" @update:value="preferences.learningContext = $event" :rows="2" placeholder="例如:尚不识谱,有打击乐器" :disabled="working" />
           <div class="preference-grid">
           <div class="preference-grid">
             <label
             <label
               ><span>素材</span
               ><span>素材</span
@@ -101,9 +101,10 @@
       </div>
       </div>
 
 
       <div v-else-if="workflowStep === 2 && outline" class="outline unified-review">
       <div v-else-if="workflowStep === 2 && outline" class="outline unified-review">
-        <details class="teaching-review" :open="!outlineConfirmed">
+        <details v-if="teachingDetailsExpanded || !outlineConfirmed" class="teaching-review" open>
           <summary>教材依据与教学主线</summary>
           <summary>教材依据与教学主线</summary>
-          <p>{{ reliableResearch ? "已检索到匹配教材的依据,请确认本课教学范围。" : "教材尚未精确匹配,暂不能生成授课成品。请返回核对课程信息后重新检索。" }}</p>
+          <p>{{ reliableResearch ? "已检索到匹配教材的依据,可在这里查看教学安排。" : "教材尚未精确匹配,暂不能生成授课成品。请返回核对课程信息后重新检索。" }}</p>
+          <p v-if="displayedConverted">必讲内容 {{ plannedMinutes }} 分钟 · 课时设置 {{ preferences.lessonMinutes }} 分钟</p>
           <p>{{ outline.textbookResearch?.contentSummary }}</p>
           <p>{{ outline.textbookResearch?.contentSummary }}</p>
           <p v-for="item in outline.textbookResearch?.missingEvidence || []" :key="item">待核对:{{ item }}</p>
           <p v-for="item in outline.textbookResearch?.missingEvidence || []" :key="item">待核对:{{ item }}</p>
           <a v-for="source in researchSources" :key="source.url" :href="source.url" target="_blank" rel="noopener noreferrer">{{ source.title || "教材来源" }}</a>
           <a v-for="source in researchSources" :key="source.url" :href="source.url" target="_blank" rel="noopener noreferrer">{{ source.title || "教材来源" }}</a>
@@ -116,22 +117,25 @@
             </label>
             </label>
           </div>
           </div>
         </details>
         </details>
-        <section v-if="displayedConverted && draftQuality" class="quality-panel" aria-live="polite">
-          <strong>{{ draftQuality.passed ? "结构与资源检查通过 · 请试讲确认" : `待修正 ${draftQuality.errors.length} 项,暂不能作为授课成品应用` }}</strong>
-          <span>必讲 {{ plannedMinutes }} / {{ preferences.lessonMinutes }} 分钟</span>
-          <details v-if="draftQuality.issues.length" :open="!draftQuality.passed">
-            <summary>查看检查结果(规则检查,不代表教学准确率)</summary>
-            <button v-for="(issue, index) in draftQuality.issues" :key="index" type="button" @click="issue.pageNo && selectWorkspacePage(issue.pageNo - 1)">
-              {{ issue.severity === 'error' ? '需修正' : '建议' }} · {{ issue.pageNo ? `第${issue.pageNo}页:` : '' }}{{ issue.message }}
-            </button>
-          </details>
+        <div v-if="displayedConverted && working" class="loading-strip" aria-live="polite"><span class="loading-dot"></span>正在完善课件,已有内容会保留</div>
+        <section v-else-if="displayedConverted && (draftQuality?.errors.length || serverReviewMessage)" class="draft-notice" aria-live="polite">
+          <span>初稿已保留,{{ draftQuality?.errors.length ? `有${draftQuality.errors.length}处待完善` : '还有内容待复核' }}</span>
+          <button type="button" @click="qualityDetailsExpanded = !qualityDetailsExpanded">{{ qualityDetailsExpanded ? '收起' : '查看问题' }}</button>
+          <Button :disabled="working" @click="repairDraft">自动完善</Button>
         </section>
         </section>
+        <div v-if="qualityDetailsExpanded && displayedConverted" class="quality-panel">
+          <p v-if="serverReviewMessage">{{ serverReviewMessage }}</p>
+          <button v-for="(issue, index) in draftQuality?.errors || []" :key="index" type="button" @click="issue.pageNo && selectWorkspacePage(issue.pageNo - 1)">
+            {{ issue.pageNo ? `第${issue.pageNo}页:` : '' }}{{ issue.message }}
+          </button>
+        </div>
         <div class="sample-stage-head">
         <div class="sample-stage-head">
           <div class="sample-template-current">
           <div class="sample-template-current">
             <span>当前样式</span>
             <span>当前样式</span>
             <strong>{{ selectedTemplate.name }}</strong>
             <strong>{{ selectedTemplate.name }}</strong>
             <small v-if="selectedTemplate.id === recommendedTemplate.id">系统推荐</small>
             <small v-if="selectedTemplate.id === recommendedTemplate.id">系统推荐</small>
           </div>
           </div>
+          <button type="button" @click="teachingDetailsExpanded = !teachingDetailsExpanded">{{ teachingDetailsExpanded ? '收起说明' : '教学说明' }}</button>
           <button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
           <button type="button" :disabled="working" @click="templatePickerExpanded = !templatePickerExpanded">
             {{ templatePickerExpanded ? "收起模板" : "更换模板" }}
             {{ templatePickerExpanded ? "收起模板" : "更换模板" }}
           </button>
           </button>
@@ -164,9 +168,9 @@
         <div v-if="!workspaceSlide && outlineConfirmed" :class="['final-preview-placeholder', draftPreparationState]">
         <div v-if="!workspaceSlide && outlineConfirmed" :class="['final-preview-placeholder', draftPreparationState]">
           <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
           <span v-if="draftPreparationState === 'optimizing'" class="loading-dot"></span>
           <span v-else class="placeholder-icon">!</span>
           <span v-else class="placeholder-icon">!</span>
-          <strong>{{ draftPreparationState === "failed" ? "课件生成失败" : generationStatusTitle }}</strong>
+          <strong>{{ draftPreparationState === "failed" ? "这次生成未完成" : generationStatusTitle }}</strong>
           <small>{{ draftPreparationState === "failed" ? draftFailureMessage : "完成后可直接预览、编辑和应用" }}</small>
           <small>{{ draftPreparationState === "failed" ? draftFailureMessage : "完成后可直接预览、编辑和应用" }}</small>
-          <Button v-if="draftPreparationState === 'failed'" type="primary" @click="regenerateFinalPreview">重生成</Button>
+          <Button v-if="draftPreparationState === 'failed'" type="primary" @click="regenerateFinalPreview">重生成</Button>
         </div>
         </div>
 
 
         <div v-if="workspaceSlide" class="unified-review-workspace">
         <div v-if="workspaceSlide" class="unified-review-workspace">
@@ -192,10 +196,10 @@
               </div>
               </div>
               <small>{{ canEditWorkspaceText ? "双击预览中的文字即可编辑" : "系统处理完成后可直接编辑文字" }}</small>
               <small>{{ canEditWorkspaceText ? "双击预览中的文字即可编辑" : "系统处理完成后可直接编辑文字" }}</small>
             </div>
             </div>
-            <div class="workspace-canvas-shell">
+            <div ref="workspaceCanvas" class="workspace-canvas-shell">
               <ThumbnailSlide
               <ThumbnailSlide
                 :slide="workspaceSlide"
                 :slide="workspaceSlide"
-                :size="720"
+                :size="workspaceCanvasSize"
                 :show-background-error="true"
                 :show-background-error="true"
                 :editable-text="true"
                 :editable-text="true"
                 :edit-disabled="!canEditWorkspaceText"
                 :edit-disabled="!canEditWorkspaceText"
@@ -209,7 +213,8 @@
               </div>
               </div>
             </div>
             </div>
             <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
             <div v-if="sampleBackgroundState.status === 'error'" class="template-load-error">模板背景加载失败,请更换模板或稍后重试</div>
-            <div class="sample-feedback workspace-feedback">
+            <details class="sample-feedback workspace-feedback">
+              <summary>调整当前页</summary>
               <label><input v-model="editTeachingContent" type="checkbox" :disabled="working" />同时修改本页教学内容(保留其他页面)</label>
               <label><input v-model="editTeachingContent" type="checkbox" :disabled="working" />同时修改本页教学内容(保留其他页面)</label>
               <div class="feedback-label">图片或版式需要调整?</div>
               <div class="feedback-label">图片或版式需要调整?</div>
               <div class="feedback-chips">
               <div class="feedback-chips">
@@ -228,7 +233,7 @@
                 <textarea v-model="sampleFeedbackText" :disabled="working" placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
                 <textarea v-model="sampleFeedbackText" :disabled="working" placeholder="说明希望如何调整,文字可直接在上方预览中双击修改"></textarea>
                 <Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
                 <Button @click="adjustCurrentPage" :disabled="working">调整当前页</Button>
               </div>
               </div>
-            </div>
+            </details>
             <details class="teaching-review" v-if="workspacePage">
             <details class="teaching-review" v-if="workspacePage">
               <summary>教师备注、用时与课堂资源</summary>
               <summary>教师备注、用时与课堂资源</summary>
               <label>用时(分钟)<input type="number" min="1" max="60" v-model.number="workspacePage.durationMinutes" :disabled="working" @input="refreshEditedDraft" /></label>
               <label>用时(分钟)<input type="number" min="1" max="60" v-model.number="workspacePage.durationMinutes" :disabled="working" @input="refreshEditedDraft" /></label>
@@ -244,7 +249,7 @@
       </div>
       </div>
 
 
       <div class="footer">
       <div class="footer">
-        <Button @click="emit('update:visible', false)" :disabled="working">取消</Button>
+        <Button @click="emit('update:visible', false)" :disabled="working">关闭</Button>
         <template v-if="isSetupStep">
         <template v-if="isSetupStep">
           <Button v-if="outline" @click="continueToFinalPreview" :disabled="working">{{ displayedConverted ? "继续预览" : "继续生成" }}</Button>
           <Button v-if="outline" @click="continueToFinalPreview" :disabled="working">{{ displayedConverted ? "继续预览" : "继续生成" }}</Button>
           <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
           <Button type="primary" @click="handleOutline" :disabled="working">{{ outline ? "按当前设置重新生成" : "开始生成课件" }}</Button>
@@ -252,12 +257,15 @@
         <template v-else-if="isStyleStep">
         <template v-else-if="isStyleStep">
           <Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
           <Button @click="workflowStep = 1" :disabled="working">返回修改需求</Button>
           <Button v-if="!outlineConfirmed" type="primary" :disabled="working || !reliableResearch" @click="confirmOutline">确认教学安排并生成</Button>
           <Button v-if="!outlineConfirmed" type="primary" :disabled="working || !reliableResearch" @click="confirmOutline">确认教学安排并生成</Button>
-          <Button v-if="displayedConverted" :disabled="working" @click="saveDraftExplicitly">保存初稿</Button>
-          <Button v-if="previousCourseware" :disabled="working" @click="restorePreviousCourseware">恢复应用前课件</Button>
+          <span v-if="displayedConverted" class="draft-save-status">{{ draftSaved ? '草稿已保存' : '草稿未保存' }}</span>
+          <details v-if="displayedConverted" class="more-actions"><summary>更多</summary><div class="more-actions-menu">
+            <Button :disabled="working" @click="saveDraftExplicitly">保存初稿</Button>
+            <Button v-if="previousCourseware" :disabled="working" @click="restorePreviousCourseware">恢复应用前课件</Button>
+          </div></details>
           <Button
           <Button
             v-if="displayedConverted && draftPreparationState === 'ready'"
             v-if="displayedConverted && draftPreparationState === 'ready'"
             type="primary"
             type="primary"
-            :disabled="working || !draftQuality?.passed || sampleBackgroundState.status === 'error'"
+            :disabled="working || !draftQuality?.passed || Boolean(serverReviewMessage) || sampleBackgroundState.status === 'error'"
             @click="applyDraft"
             @click="applyDraft"
             >应用到当前课件</Button
             >应用到当前课件</Button
           >
           >
@@ -322,6 +330,24 @@ const { addHistorySnapshot } = useHistorySnapshot()
 const extraPrompt = ref("")
 const extraPrompt = ref("")
 const outline = ref<AiCoursewareOutline>()
 const outline = ref<AiCoursewareOutline>()
 const outlineConfirmed = ref(false)
 const outlineConfirmed = ref(false)
+const teachingDetailsExpanded = ref(false)
+const qualityDetailsExpanded = ref(false)
+const serverReviewMessage = ref("")
+const draftSaved = ref(false)
+const workspaceCanvas = ref<HTMLElement>()
+const canvasWidth = ref(720)
+const canvasHeight = ref(405)
+watch(workspaceCanvas, (element, _, onCleanup) => {
+  if (!element) return
+  const observer = new ResizeObserver(([entry]) => {
+    canvasWidth.value = entry.contentRect.width
+    canvasHeight.value = entry.contentRect.height
+  })
+  observer.observe(element)
+  onCleanup(() => observer.disconnect())
+})
+// 同时约束宽高,避免小窗口里只显示幻灯片中间的一截。
+const workspaceCanvasSize = computed(() => Math.max(1, Math.min(canvasWidth.value, canvasHeight.value / slidesStore.viewportRatio)))
 const editTeachingContent = ref(false)
 const editTeachingContent = ref(false)
 const availableResources = ref<AiCoursewareResourceBinding[]>([])
 const availableResources = ref<AiCoursewareResourceBinding[]>([])
 const previousCourseware = ref<AiDraftRecord["backup"]>()
 const previousCourseware = ref<AiDraftRecord["backup"]>()
@@ -573,6 +599,8 @@ async function handleOutline() {
     displayedConverted.value = undefined
     displayedConverted.value = undefined
     displayedTemplateId.value = undefined
     displayedTemplateId.value = undefined
     draftQuality.value = undefined
     draftQuality.value = undefined
+    serverReviewMessage.value = ""
+    qualityDetailsExpanded.value = false
     draftPreparationState.value = "idle"
     draftPreparationState.value = "idle"
     draftFailureMessage.value = "请稍后重试"
     draftFailureMessage.value = "请稍后重试"
     outline.value = res.data as AiCoursewareOutline
     outline.value = res.data as AiCoursewareOutline
@@ -586,6 +614,7 @@ async function handleOutline() {
     draftPreparationState.value = "idle"
     draftPreparationState.value = "idle"
     workflowStep.value = 2
     workflowStep.value = 2
     await persistDraft()
     await persistDraft()
+    if (reliableResearch.value) await confirmOutline()
   }
   }
 }
 }
 
 
@@ -595,7 +624,7 @@ async function continueToFinalPreview() {
     draftPreparationState.value = "ready"
     draftPreparationState.value = "ready"
     return
     return
   }
   }
-  if (outlineConfirmed.value) await confirmSamplesAndGenerate()
+  if (outlineConfirmed.value) await handleGenerate()
 }
 }
 
 
 async function regenerateFinalPreview() {
 async function regenerateFinalPreview() {
@@ -631,16 +660,22 @@ function handleFinalPreparationFailure(message = "自动生成和优化未能完
   draftPreparationState.value = "failed"
   draftPreparationState.value = "failed"
 }
 }
 
 
+async function repairDraft() {
+  if (working.value || !displayedOutline.value) return
+  draftOutline.value = cloneCoursewareData(displayedOutline.value)
+  await handleGenerate(true)
+}
+
 function cloneCoursewareData<T>(value: T): T {
 function cloneCoursewareData<T>(value: T): T {
   return JSON.parse(JSON.stringify(value)) as T
   return JSON.parse(JSON.stringify(value)) as T
 }
 }
 
 
-async function handleGenerate() {
+async function handleGenerate(repairOnly = false) {
   const sourceOutline = draftOutline.value || outline.value
   const sourceOutline = draftOutline.value || outline.value
   if (!sourceOutline || !pptWork.id) return
   if (!sourceOutline || !pptWork.id) return
 
 
   draftConverted.value = undefined
   draftConverted.value = undefined
-  draftQuality.value = undefined
+  if (!displayedConverted.value) draftQuality.value = undefined
   draftPreparationState.value = "optimizing"
   draftPreparationState.value = "optimizing"
   draftFailureMessage.value = "请稍后重试"
   draftFailureMessage.value = "请稍后重试"
   sourceOutline.style = selectedStyle.value
   sourceOutline.style = selectedStyle.value
@@ -651,7 +686,8 @@ async function handleGenerate() {
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     fromType: queryParams.pptResourcesType || queryParams.fromType,
     outline: sourceOutline,
     outline: sourceOutline,
     templateId: selectedTemplateId.value,
     templateId: selectedTemplateId.value,
-    visualPrompt: buildVisualPrompt(),
+    visualPrompt: buildVisualPrompt() + (repairOnly ? `\n请在现有初稿上修复以下问题,保留无关内容及教师修改:${[serverReviewMessage.value, ...(draftQuality.value?.errors || []).map(issue => issue.message)].filter(Boolean).join(';')}` : ''),
+    repairOnly,
     preferences: { ...preferences }
     preferences: { ...preferences }
   })
   })
   if (res.code !== 200) {
   if (res.code !== 200) {
@@ -679,7 +715,7 @@ async function fetchTask(taskId: string) {
     return
     return
   }
   }
   taskInfo.value = res.data as AiCoursewareTaskInfo
   taskInfo.value = res.data as AiCoursewareTaskInfo
-  if (taskInfo.value.status === "DONE") {
+  if (taskInfo.value.status === "DONE" || taskInfo.value.status === "DRAFT") {
     clearPollTimer()
     clearPollTimer()
     await loadDraft(taskId)
     await loadDraft(taskId)
   } else if (taskInfo.value.status === "FAILED") {
   } else if (taskInfo.value.status === "FAILED") {
@@ -724,6 +760,7 @@ async function loadDraft(taskId: string) {
     return
     return
   }
   }
   draftOutline.value = result.outline
   draftOutline.value = result.outline
+  serverReviewMessage.value = result.reviewMessage || ""
   availableResources.value = Array.isArray(result.context?.resources) ? result.context.resources : []
   availableResources.value = Array.isArray(result.context?.resources) ? result.context.resources : []
   restoreProtectedPageContent(draftOutline.value)
   restoreProtectedPageContent(draftOutline.value)
   draftOutline.value.templateId = selectedTemplateId.value
   draftOutline.value.templateId = selectedTemplateId.value
@@ -783,7 +820,7 @@ async function applyDraft() {
   draftQuality.value = inspectAiPptQuality(displayedOutline.value, displayedConverted.value.slides, preferences)
   draftQuality.value = inspectAiPptQuality(displayedOutline.value, displayedConverted.value.slides, preferences)
   appendResourceIssues(resourceIssues)
   appendResourceIssues(resourceIssues)
   draftPreparationState.value = "ready"
   draftPreparationState.value = "ready"
-  if (!draftQuality.value.passed) return
+  if (!draftQuality.value.passed || serverReviewMessage.value) return
   previousCourseware.value = cloneCoursewareData({ title: slidesStore.title, theme: slidesStore.theme, slides: slidesStore.slides, width: slidesStore.viewportSize, ratio: slidesStore.viewportRatio })
   previousCourseware.value = cloneCoursewareData({ title: slidesStore.title, theme: slidesStore.theme, slides: slidesStore.slides, width: slidesStore.viewportSize, ratio: slidesStore.viewportRatio })
   if (!await persistDraft()) return
   if (!await persistDraft()) return
   slidesStore.setViewportSize(1920)
   slidesStore.setViewportSize(1920)
@@ -802,27 +839,6 @@ async function adjustCurrentPage() {
   await regenerateDraftPage(selectedDraftPageIndex.value)
   await regenerateDraftPage(selectedDraftPageIndex.value)
 }
 }
 
 
-async function confirmSamplesAndGenerate() {
-  const slides = previewSlides.value || []
-  if (!outline.value || slides.length !== outline.value.pages.length) {
-    ElMessage.error("页面预览尚未准备完成,请稍后重试")
-    return
-  }
-
-  const backgroundUrls = Array.from(
-    new Set(slides.map(slide => (slide.background?.type === "image" ? resolveTemplateAssetUrl(slide.background.image) : "")).filter(Boolean))
-  )
-
-  try {
-    await Promise.all(backgroundUrls.map(preloadImage))
-  } catch {
-    handleFinalPreparationFailure("模板背景加载失败")
-    return
-  }
-
-  await handleGenerate()
-}
-
 function handleSampleBackgroundLoadState(payload: { status: "none" | "loading" | "loaded" | "error"; url: string }) {
 function handleSampleBackgroundLoadState(payload: { status: "none" | "loading" | "loaded" | "error"; url: string }) {
   sampleBackgroundState.value = payload
   sampleBackgroundState.value = payload
   if (payload.status === "error") {
   if (payload.status === "error") {
@@ -876,17 +892,6 @@ function templateCoverStyle(templateId: AiCoursewareTemplateId): Record<string,
   return { background: cover?.background || selectedTemplate.value.palettes[0].background }
   return { background: cover?.background || selectedTemplate.value.palettes[0].background }
 }
 }
 
 
-function preloadImage(url: string) {
-  if (!url || typeof Image === "undefined") return Promise.resolve()
-  return new Promise<void>((resolve, reject) => {
-    const image = new Image()
-    const timer = window.setTimeout(() => reject(new Error("图片加载超时")), 15000)
-    image.onload = () => { window.clearTimeout(timer); resolve() }
-    image.onerror = () => { window.clearTimeout(timer); reject(new Error(`failed to load ${url}`)) }
-    image.src = url
-  })
-}
-
 function templateFit(templateId: AiCoursewareTemplateId) {
 function templateFit(templateId: AiCoursewareTemplateId) {
   return outline.value
   return outline.value
     ? scoreAiCoursewareTemplate(outline.value, templateId)
     ? scoreAiCoursewareTemplate(outline.value, templateId)
@@ -1009,7 +1014,7 @@ function buildExtraPrompt() {
 async function confirmOutline() {
 async function confirmOutline() {
   if (!reliableResearch.value || working.value) return
   if (!reliableResearch.value || working.value) return
   outlineConfirmed.value = true
   outlineConfirmed.value = true
-  await confirmSamplesAndGenerate()
+  await handleGenerate()
 }
 }
 
 
 async function refreshEditedDraft() {
 async function refreshEditedDraft() {
@@ -1045,9 +1050,11 @@ function toggleResource(resource: AiCoursewareResourceBinding) {
 let saveQueue: Promise<unknown> = Promise.resolve()
 let saveQueue: Promise<unknown> = Promise.resolve()
 async function persistDraft() {
 async function persistDraft() {
   if (!pptWork.id) return false
   if (!pptWork.id) return false
+  draftSaved.value = false
   const snapshot = cloneCoursewareData({ outline: displayedOutline.value || draftOutline.value || outline.value,
   const snapshot = cloneCoursewareData({ outline: displayedOutline.value || draftOutline.value || outline.value,
     converted: displayedConverted.value, preferences: { ...preferences }, extraPrompt: extraPrompt.value,
     converted: displayedConverted.value, preferences: { ...preferences }, extraPrompt: extraPrompt.value,
-    taskId: taskInfo.value?.taskId, resources: availableResources.value, backup: previousCourseware.value,
+    taskId: taskInfo.value && ['PENDING', 'RUNNING'].includes(taskInfo.value.status) ? taskInfo.value.taskId : undefined,
+    reviewMessage: serverReviewMessage.value, resources: availableResources.value, backup: previousCourseware.value,
     protectedContent: Array.from(protectedPageContent.entries()) })
     protectedContent: Array.from(protectedPageContent.entries()) })
   const id = String(pptWork.id)
   const id = String(pptWork.id)
   const source = queryParams.pptResourcesType || queryParams.fromType || "TEACHER"
   const source = queryParams.pptResourcesType || queryParams.fromType || "TEACHER"
@@ -1055,8 +1062,10 @@ async function persistDraft() {
     const key = await draftKey(id, source)
     const key = await draftKey(id, source)
     saveQueue = saveQueue.catch(() => undefined).then(() => saveAiDraft({ ...snapshot, key, updatedAt: Date.now() }))
     saveQueue = saveQueue.catch(() => undefined).then(() => saveAiDraft({ ...snapshot, key, updatedAt: Date.now() }))
     await saveQueue
     await saveQueue
+    draftSaved.value = true
     return true
     return true
   } catch {
   } catch {
+    draftSaved.value = false
     ElMessage.warning("本机草稿保存失败,请勿刷新页面,并及时手动保存课件")
     ElMessage.warning("本机草稿保存失败,请勿刷新页面,并及时手动保存课件")
     return false
     return false
   }
   }
@@ -1079,6 +1088,8 @@ async function restoreSavedDraft() {
     protectedPageContent.clear()
     protectedPageContent.clear()
     for (const [pageNo, content] of saved.protectedContent || []) protectedPageContent.set(pageNo, content)
     for (const [pageNo, content] of saved.protectedContent || []) protectedPageContent.set(pageNo, content)
     previousCourseware.value = saved.backup
     previousCourseware.value = saved.backup
+    serverReviewMessage.value = saved.reviewMessage || ""
+    draftSaved.value = true
     outline.value = saved.outline
     outline.value = saved.outline
     selectedTemplateId.value = getAiCoursewareTemplate(saved.outline.templateId).id
     selectedTemplateId.value = getAiCoursewareTemplate(saved.outline.templateId).id
     draftOutline.value = cloneCoursewareData(saved.outline)
     draftOutline.value = cloneCoursewareData(saved.outline)
@@ -1157,6 +1168,41 @@ function shortProgressLabel(message?: string) {
 </script>
 </script>
 
 
 <style lang="scss" scoped>
 <style lang="scss" scoped>
+.draft-notice {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 8px 12px;
+  margin: 8px 0;
+  border-radius: 6px;
+  background: #fff7e8;
+  color: #76552b;
+  font-size: 13px;
+  flex-shrink: 0;
+  > span { flex: 1; }
+  > button:not(.button) { color: #1677d2; border: 0; background: none; cursor: pointer; }
+}
+.draft-save-status { font-size: 12px; color: #7b8794; margin-right: auto; }
+.more-actions {
+  position: relative;
+  font-size: 13px;
+  summary { cursor: pointer; color: #64748b; }
+}
+.more-actions-menu {
+  position: absolute;
+  right: 0;
+  bottom: 26px;
+  z-index: 10;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+  width: 160px;
+  padding: 8px;
+  border: 1px solid #e1e8f0;
+  border-radius: 6px;
+  background: #fff;
+  box-shadow: 0 4px 18px rgba(31, 41, 51, 0.12);
+}
 .teaching-review, .quality-panel {
 .teaching-review, .quality-panel {
   margin: 10px 0;
   margin: 10px 0;
   padding: 12px 16px;
   padding: 12px 16px;
@@ -1683,6 +1729,7 @@ function shortProgressLabel(message?: string) {
   min-height: 0;
   min-height: 0;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
+  overflow: auto;
   padding: 10px;
   padding: 10px;
   border: 1px solid #dfe6ee;
   border: 1px solid #dfe6ee;
   border-radius: 8px;
   border-radius: 8px;
@@ -1718,12 +1765,12 @@ function shortProgressLabel(message?: string) {
 }
 }
 .workspace-canvas-shell {
 .workspace-canvas-shell {
   position: relative;
   position: relative;
-  min-height: 0;
-  flex: 1 1 auto;
+  min-height: 180px;
+  flex: 1 0 180px;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   justify-content: center;
   justify-content: center;
-  overflow: auto;
+  overflow: hidden;
   padding: 10px;
   padding: 10px;
   border-radius: 7px;
   border-radius: 7px;
   background: #eef2f7;
   background: #eef2f7;
@@ -1759,6 +1806,14 @@ function shortProgressLabel(message?: string) {
   flex: 0 0 auto;
   flex: 0 0 auto;
   margin-top: 8px;
   margin-top: 8px;
   padding-top: 8px;
   padding-top: 8px;
+  summary { color: #52616b; font-size: 13px; cursor: pointer; }
+  &[open] summary { margin-bottom: 10px; }
+}
+.workspace-preview-column > .teaching-review {
+  flex-shrink: 0;
+  margin-bottom: 0;
+  padding: 8px 12px;
+  summary { font-size: 13px; }
 }
 }
 .sample-stage-head {
 .sample-stage-head {
   display: flex;
   display: flex;
@@ -1780,6 +1835,7 @@ function shortProgressLabel(message?: string) {
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
   min-width: 0;
   min-width: 0;
+  margin-right: auto;
   gap: 7px;
   gap: 7px;
   color: #657381;
   color: #657381;
   font-size: 12px;
   font-size: 12px;

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