aiPptDialogTest.html 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. <!doctype html>
  2. <html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>AI课件流程回归</title></head><body><div id="app"></div>
  3. <script type="module">
  4. import { createApp, h, ref, nextTick } from "vue"
  5. import { createRouter, createMemoryHistory } from "vue-router"
  6. import { store, useSlidesStore } from "/src/store/index.ts"
  7. import "/src/router/index.ts"
  8. import { httpAxios } from "/src/api/ApiInstance.ts"
  9. import { setToken } from "/src/libs/auth.ts"
  10. import AiDialog from "/src/views/Editor/EditorHeader/AiCoursewareDialog.vue"
  11. import Editor from "/src/views/Editor/index.vue"
  12. import usePptWork from "/src/store/pptWork.ts"
  13. import { useSnapshotStore } from "/src/store/index.ts"
  14. import { inspectAiPptResources } from "/src/utils/aiPptResources.ts"
  15. import { draftKey, loadAiDraft, saveAiDraft } from "/src/utils/aiCoursewareDraft.ts"
  16. import useAiCoursewarePageRepair from "/src/hooks/useAiCoursewarePageRepair.ts"
  17. import { aiCoursewareToSlides } from "/src/utils/aiCoursewareToSlides.ts"
  18. import "/src/assets/styles/global.scss"
  19. import "/src/assets/styles/elementTheme.scss"
  20. import "/src/assets/styles/prosemirror.scss"
  21. // 所有业务接口使用本地夹具,不调用生成服务,不修改任何线上课件。
  22. const scenario = new URLSearchParams(location.search).get("scenario") || "normal"
  23. setToken(`local-ai-ppt-regression-${scenario}`)
  24. const generated = ref(0)
  25. const outlineCalls = ref(0), savedCalls = ref(0), submitted = ref(null)
  26. const savedText = ref("")
  27. let pollCount = 0
  28. const repairs = ref(0)
  29. const historicalPolls = ref(0)
  30. let repaired = false
  31. const resourceCheck = ref("检查中")
  32. const runtimeError = ref("")
  33. const types = ["cover", "scene_intro", "objectives", "music_knowledge", "rhythm_practice", "performance_activity", "practice_check", "creative_activity", "summary"]
  34. const layouts = ["cover_scene", "hero_focus", "objectives", "split_content", "diagram_focus", "activity", "text_focus", "activity", "summary"]
  35. const outline = {
  36. title: "声音强弱课堂测试", gradeName: "一年级", textbookName: "本地测试教材", chapterName: "声音的强弱", courseType: "music", templateId: "ethnic-flower",
  37. textbookResearch: { exactMatch: true, confidence: "HIGH", contentSummary: "仅用于软件回归的教学夹具", teachingObjectives: ["能用声音表现强弱"], sources: [{ title: "本地测试依据(非真实教材认证)", url: "https://example.com/test-only" }] },
  38. pages: types.map((type, index) => ({ pageNo:index+1, type, title: index === 0 ? "声音强弱课堂测试" : `任务${index}:表现声音强弱`,
  39. teachingBrief: index === 0 ? "从山谷故事引入,邀请学生寻找身边的声音" : "听两组声音,辨别强弱,再用拍手模仿,并说明判断理由",
  40. studentContent:{ points:["用声音表现强弱变化", "说出判断的理由"] }, durationMinutes:index===0?8:4, objectiveIds:["1"],
  41. teachingPurpose:"表现声音强弱", learningEvidence:"能表现强弱并说明理由", speakerNotes:"示范后请学生练习,根据表现及时反馈。",
  42. classroomAction:{studentTask:"完成声音强弱挑战", teacherInstruction:"示范强弱对比,邀请学生练习后互评"},
  43. interaction:type==="practice_check"?{question:"哪种方式声音更轻?",options:["轻声哼唱","大声呼喊"],answer:"轻声哼唱",explanation:"根据声音强弱判断"}:undefined,
  44. visualPlan:{layout:layouts[index]}, imageKeywords:[] }))
  45. }
  46. if (scenario === "failed-resume") {
  47. const key = await draftKey("local-ai-ppt-regression", "TEACHER")
  48. if (!await loadAiDraft(key)) await saveAiDraft({
  49. key, updatedAt: Date.now(), outline: structuredClone(outline), taskId: "historical-failed-task", extraPrompt: "",
  50. preferences: { minPages: 9, maxPages: 12, lessonMinutes: 40 }
  51. })
  52. }
  53. httpAxios.axioseRquest = async config => {
  54. let data
  55. if (config.url.endsWith("/outline")) { outlineCalls.value++; data = structuredClone(outline) }
  56. else if (config.url.endsWith("/generate")) {
  57. generated.value++
  58. submitted.value = JSON.parse(JSON.stringify(config.data))
  59. pollCount = 0
  60. if (config.data.repairOnly || config.data.regeneratePageNo) { repairs.value++; repaired=true }
  61. data={taskId:`local-task-${generated.value}`,status:"PENDING",progress:0}
  62. }
  63. else if (config.url.endsWith("/result")) {
  64. data={outline:JSON.parse(JSON.stringify(submitted.value?.outline || outline)),context:{resources:[]}}
  65. data.outline.pages.forEach((page, index) => {
  66. const fixture = outline.pages[index % outline.pages.length]
  67. Object.assign(page, { ...structuredClone(fixture), pageNo: index + 1, title: page.title, teachingBrief: page.teachingBrief })
  68. page.studentContent = { points: ['哪种声音更轻?', '试着用拍手表现出来'] }
  69. page.bullets = [...page.studentContent.points]
  70. page.objectiveIds=["1"]
  71. })
  72. if (scenario.includes("draft") && !repaired) {
  73. data.outline.pages.forEach(page => page.objectiveIds=[])
  74. data.reviewMessage="声音强弱的练习与评价尚未完整关联"
  75. }
  76. if (scenario === 'whole-repair-draft') data.reviewMessage = '第2页:学生画面缺少具体范例;整课一致性复核暂未完成'
  77. if (scenario === 'page-repair') {
  78. const page = data.outline.pages[submitted.value.regeneratePageNo - 1]
  79. page.studentContent = { points: ['重新生成后的课堂任务', '观察图片,说说声音的变化'] }
  80. page.bullets = [...page.studentContent.points]
  81. page.image = { url: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', width: 1, height: 1 }
  82. }
  83. }
  84. else if (config.url.endsWith("/task/historical-failed-task")) {
  85. historicalPolls.value++
  86. data={taskId:"historical-failed-task",status:"FAILED",progress:50,message:"教学目标1缺少对应实践或评价任务"}
  87. }
  88. else if (config.url.includes("/task/")) {
  89. if (scenario === 'page-repair') await new Promise(resolve => setTimeout(resolve, 1500))
  90. pollCount++
  91. if (scenario.includes("network") && pollCount === 1) throw new Error("测试网络中断")
  92. data={taskId:`local-task-${generated.value}`,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") ? "测试:生成服务暂不可用" : "正在完善课堂任务"}
  93. }
  94. else throw new Error("测试禁止未声明的业务请求")
  95. return {data:{code:200,data}}
  96. }
  97. const router=createRouter({history:createMemoryHistory(),routes:[{path:"/",component:{render:()=>null}}]})
  98. await router.push("/?id=local-ai-ppt-regression")
  99. await router.isReady()
  100. const app=createApp({setup(){
  101. const visible=ref(false)
  102. const slidesStore=useSlidesStore()
  103. const pptWork=usePptWork()
  104. const repair = useAiCoursewarePageRepair()
  105. if (scenario.startsWith('whole-repair')) {
  106. const fixture = structuredClone(outline)
  107. fixture.templateId = 'blank'
  108. slidesStore.setSlides(aiCoursewareToSlides(fixture).slides)
  109. const originalIds = slidesStore.slides.map(slide => slide.id).join(',')
  110. repair.generated([{ code: 'teaching-review', severity: 'error', message: '第2页:请补充具体课堂任务' }], fixture,
  111. { lessonMinutes:40, minPages:9, maxPages:9, resourceMode:'teacher_supplement', enableAnimations:false })
  112. return () => h('main', [
  113. h('h1', '整课完善回归(本地模拟接口)'),
  114. h('button', { disabled: repair.repairingPage.value !== undefined, onClick: () => repair.repair(0) }, '完善整课'),
  115. h('button', { onClick: () => { slidesStore.slides[1].elements.find(e => e.type === 'text').content = '<p>老师手动修改的内容</p>' } }, '手动修改第2页'),
  116. h('p', { id: 'repair-state' }, `状态:${repair.repairingPage.value === undefined ? '空闲' : '完善中'};完善请求:${repairs.value};repairOnly:${submitted.value?.repairOnly === true}`),
  117. h('p', { id: 'repair-error' }, repair.error.value),
  118. h('p', { id: 'repair-issues' }, JSON.stringify(repair.issues.value)),
  119. h('p', { id: 'repair-content' }, slidesStore.slides[1].elements.filter(e => e.type === 'text').map(e => e.content.replace(/<[^>]+>/g, '')).join(';')),
  120. h('p', { id: 'repair-ids' }, `页面标识${slidesStore.slides.map(slide => slide.id).join(',') === originalIds ? '保持不变' : '变化'}`),
  121. h('button', { onClick: () => useSnapshotStore().unDo() }, '测试撤销')
  122. ])
  123. }
  124. if (scenario === 'page-repair') {
  125. const fixture = structuredClone(outline)
  126. fixture.templateId = 'blank'
  127. fixture.pages[1].requiredVisual = true
  128. fixture.pages[1].visualPlan.imagePurpose = 'scene'
  129. slidesStore.setSlides(aiCoursewareToSlides(fixture).slides)
  130. const beforeOthers = JSON.stringify(slidesStore.slides.filter((_, index) => index !== 1))
  131. repair.generated([{ code: 'missing_required_visual', severity: 'error', pageNo: 2, message: '缺少配图' }], fixture, { lessonMinutes:40,minPages:9,maxPages:9,assetStyle:'illustration',sourceImagePolicy:'balanced',enableAnimations:false })
  132. const pageText = () => slidesStore.slides[1].elements.filter(e => e.type === 'text').map(e => e.content.replace(/<[^>]+>/g, '')).join(';')
  133. return () => h('main', [
  134. h('h1', '单页生成回归(本地模拟接口)'),
  135. h('button', { disabled: !!repair.repairingPage.value, onClick: () => repair.repair(2) }, '重新生成第2页'),
  136. h('button', { onClick: () => { slidesStore.slides[1].elements.find(e => e.type === 'text').content = '<p>生成期间手动修改</p>' } }, '修改第2页'),
  137. h('p', { id: 'repair-state' }, `状态:${repair.repairingPage.value ? '生成中' : '空闲'};重试次数:${repairs.value}`),
  138. h('p', { id: 'repair-error' }, repair.error.value),
  139. h('p', { id: 'repair-content' }, pageText()),
  140. h('p', { id: 'repair-image' }, `本页图片:${slidesStore.slides[1].elements.filter(e => e.type === 'image').length}`),
  141. h('p', { id: 'other-pages' }, `其他页面${JSON.stringify(slidesStore.slides.filter((_, index) => index !== 1)) === beforeOthers ? '保持不变' : '被修改'}`),
  142. h('button', { onClick: () => useSnapshotStore().unDo() }, '测试撤销')
  143. ])
  144. }
  145. // 仅隔离存储边界,按钮、编辑器和课件转换均使用正式组件。
  146. pptWork.updatePPT=async()=>{savedText.value=slidesStore.slides.flatMap(slide=>slide.elements.filter(el=>el.type==="text").map(el=>el.content.replace(/<[^>]+>/g,""))).join("、");savedCalls.value++;pptWork.isSave=true}
  147. if (!scenario.startsWith("editor")) nextTick(()=>{visible.value=true})
  148. return ()=>h("main",[
  149. runtimeError.value ? h("pre", runtimeError.value) : null,
  150. savedCalls.value ? h("p",{id:"saved-text"},`保存内容:${savedText.value}`):null,
  151. h("p",{id:"test-status"},`大纲请求:${outlineCalls.value};生成请求次数:${generated.value};完善请求:${repairs.value};保存请求:${savedCalls.value};历史失败任务查询:${historicalPolls.value};已应用页数:${slidesStore.slides.length};资源检查:${resourceCheck.value}`),
  152. 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,
  153. scenario.startsWith("editor") ? h("div",{style:"height:calc(100vh - 65px)"},[h(Editor)]) : h("button",{onClick:()=>{visible.value=true}},"打开AI课件"),
  154. scenario.startsWith("editor") ? null : h(AiDialog,{visible:visible.value,"onUpdate:visible":value=>{visible.value=value}}),
  155. h("button",{onClick:()=>useSnapshotStore().unDo()},"测试撤销")
  156. ])
  157. }})
  158. app.config.errorHandler = error => { runtimeError.value=String(error?.stack || error) }
  159. app.use(store).use(router).mount("#app")
  160. // 使用浏览器真实图片解码验证损坏资源阻断及谱图等比缩放。
  161. 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>')
  162. const score={id:"image",type:"image",src:svg,left:0,top:0,width:200,height:200}
  163. const slides=[{elements:[score,{id:"broken",type:"image",src:"data:image/png;base64,invalid",left:0,top:0,width:100,height:100}]}]
  164. const issues=await inspectAiPptResources({pages:[{resourceBindings:[{resourceType:"MUSIC",scoreImageUrl:svg}]}]},slides)
  165. resourceCheck.value=issues.length===1&&score.width===200&&score.height===50&&score.top===75?"通过(损坏图片阻断、谱图等比缩放)":"失败"
  166. </script></body></html>