| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- import { onBeforeUnmount, ref, watch } from "vue"
- import { ElMessage } from "element-plus"
- import { useMainStore, useSlidesStore, useSnapshotStore } from "@/store"
- import usePptWork from "@/store/pptWork"
- import queryParams from "@/queryParams"
- import { httpAjax } from "@/plugins/httpAjax"
- import { aiCoursewareGenerateApi, aiCoursewareTaskApi, aiCoursewareResultApi, type AiCoursewareOutline, type AiCoursewarePreferences } from "@/api/aiCourseware"
- import { assertAiPptClientCurrent } from "@/utils/aiPptClientVersion"
- import { aiCoursewareToSlides } from "@/utils/aiCoursewareToSlides"
- 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'
- const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value))
- // 单页修复与整课复核共用已有任务接口;始终保护生成期间的新编辑。
- export default function useAiCoursewarePageRepair() {
- const slides = useSlidesStore(), work = usePptWork(), snapshots = useSnapshotStore()
- const issues = ref<AiPptQualityIssue[]>([]), repairingPage = ref<number>()
- const error = ref("")
- let outline: AiCoursewareOutline | undefined, preferences: AiCoursewarePreferences | undefined
- let slideIds: string[] = [], version = 0
- let baseline: string[] = []
- const reset = () => { version++; outline = undefined; preferences = undefined; slideIds = []; baseline = []; issues.value = []; repairingPage.value = undefined; error.value = "" }
- watch(() => work.id, reset)
- onBeforeUnmount(reset)
- function generated(nextIssues: AiPptQualityIssue[], nextOutline: AiCoursewareOutline, nextPreferences: AiCoursewarePreferences) {
- reset()
- issues.value = nextIssues
- outline = clone(nextOutline)
- preferences = clone(nextPreferences)
- slideIds = slides.slides.map(slide => slide.id)
- baseline = slides.slides.map(slide => JSON.stringify(slide))
- }
- function locate(pageNo: number) {
- const index = slides.slides.findIndex(slide => slide.id === slideIds[pageNo - 1])
- if (index >= 0) slides.updateSlideIndex(index)
- return index
- }
- async function repair(pageNo: number) {
- if (!outline || !preferences || repairingPage.value !== undefined) return
- const whole = pageNo === 0
- if (whole && (slides.slides.length !== baseline.length || slides.slides.some((slide, i) => JSON.stringify(slide) !== baseline[i]))) {
- error.value = '课件已有手动编辑。为保留你的修改,请使用对应问题的“重新生成本页”。'; return
- }
- const index = whole ? 0 : locate(pageNo)
- if (index < 0) { error.value = "原页面已删除,请从当前课件重新生成。"; return }
- const slideId = slideIds[pageNo - 1], before = JSON.stringify(whole ? slides.slides : slides.slides[index])
- const requestVersion = version, materialId = work.id
- const active = () => requestVersion === version && work.id === materialId
- const sourceOutline = clone(outline), prefs = clone(preferences)
- const templateId = sourceOutline.templateId || DEFAULT_AI_COURSEWARE_TEMPLATE_ID
- repairingPage.value = pageNo
- error.value = ""
- try {
- await assertAiPptClientCurrent()
- if (!active()) return
- const response = await httpAjax(aiCoursewareGenerateApi, {
- id: materialId, fromType: queryParams.pptResourcesType || queryParams.fromType || "TEACHER",
- outline: sourceOutline, templateId, preferences: prefs, ...(whole ? { repairOnly: true } : { regeneratePageNo: pageNo }),
- imageAspectRatios: getAiPptImageAspectRatios(templateId),
- visualPrompt: (whole ? '在当前草稿上完善整课,保持页数与页序,保留正确内容。' : '仅完善本页,保持教学目标。')
- + '补齐学生可见的示例、分工和操作规则,统一题干与教师备注,修正图示及排版。媒体由教师补充。待解决问题:'
- + issues.value.filter(issue => whole || !issue.pageNo || issue.pageNo === pageNo).map(issue => issue.message).join(';')
- })
- if (!active()) return
- if (response.code !== 200 || !response.data?.taskId) throw new Error(response.message || "未能开始单页生成")
- const started = Date.now()
- let draft = false
- while (active()) {
- const task = await httpAjax(aiCoursewareTaskApi, response.data.taskId)
- if (!active()) return
- if (task.code !== 200) throw new Error(task.message || "无法读取生成进度")
- if (task.data.status === "FAILED") throw new Error(task.data.message || "本页生成失败")
- if (["DONE", "DRAFT"].includes(task.data.status)) { draft = task.data.status === 'DRAFT'; break }
- if (Date.now() - started > 15 * 60 * 1000) throw new Error("本页生成超过15分钟,请稍后重试")
- await new Promise(resolve => setTimeout(resolve, 2500))
- }
- if (!active()) return
- const result = await httpAjax(aiCoursewareResultApi, response.data.taskId)
- if (!active()) return
- if (result.code !== 200 || !result.data?.outline?.pages?.[whole ? 0 : pageNo - 1]) throw new Error(result.message || "生成结果不完整")
- const resultOutline = result.data.outline as AiCoursewareOutline
- if (whole && (resultOutline.pages.length !== sourceOutline.pages.length || resultOutline.pages.some((page, i) => page.pageNo !== i + 1))) throw new Error('完善后的页数或顺序不符,已保留当前课件。')
- // 忽略后端返回的其他页面,保留本地原始教学安排。
- const nextOutline = clone(whole ? resultOutline : sourceOutline)
- if (!whole) nextOutline.pages[pageNo - 1] = resultOutline.pages[pageNo - 1]
- if (draft) {
- // 保留服务端已经完成的配图供下次续作,画布保持不变。
- outline = nextOutline
- throw new Error('课件尚未全部完成,已保留生成内容和成功配图,请继续完成。当前页面未被替换。')
- }
- await assertAiPptClientCurrent()
- if (!active()) return
- const converted = aiCoursewareToSlides(nextOutline, { templateId, enableAnimations: prefs.enableAnimations })
- const target = converted.slides[pageNo - 1]
- const report = inspectAiPptQuality(nextOutline, converted.slides, { ...prefs, minPages: nextOutline.pages.length, maxPages: nextOutline.pages.length })
- if (blockingAiPptLayoutIssues(report).some(issue => whole || issue.pageNo === pageNo)) throw new Error('生成页的文字仍然过密,已保留当前课件,请再次精简后生成。')
- const resourceIssues = await inspectAiPptResources(whole ? nextOutline : { ...nextOutline, pages: [nextOutline.pages[pageNo - 1]] }, whole ? converted.slides : [target])
- if (!active()) return
- const imageIssues = blockingImageIssues([...report.issues.filter(issue => whole || issue.pageNo === pageNo), ...resourceIssues])
- if (imageIssues.length) throw new Error('配图尚未全部完成或加载失败,当前课件已保留,请继续完成。')
- const unchanged = () => whole ? JSON.stringify(slides.slides) === before : JSON.stringify(slides.slides.find(slide => slide.id === slideId)) === before
- if (!unchanged()) throw new Error("课件在生成期间已被修改或删除,未替换当前内容,避免丢失你的编辑。")
- await snapshots.addSnapshot()
- if (!active()) return
- const latest = whole ? 0 : slides.slides.findIndex(slide => slide.id === slideId)
- if (!unchanged()) throw new Error("课件内容已变化,未覆盖当前编辑。")
- const nextSlides = whole ? converted.slides.map((slide, i) => ({ ...slide, id: slideIds[i] })) : [...slides.slides]
- if (!whole) nextSlides[latest] = { ...target, id: slideId }
- slides.setSlides(nextSlides)
- slides.updateSlideIndex(latest)
- useMainStore().setActiveElementIdList([])
- work.isSave = false
- outline = nextOutline
- 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)
- 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 })]
- await snapshots.addSnapshot()
- if (active()) {
- if (draft || issues.value.some(issue => issue.severity === 'error')) ElMessage.warning('草稿已更新,仍有待完善内容,请查看问题列表。')
- else ElMessage.success(`${whole ? '整课已完善' : `第 ${pageNo} 页已重新生成`},可撤销恢复;请核对后保存。`)
- }
- } catch (reason) {
- if (active()) error.value = reason instanceof Error ? reason.message : "本页生成失败,请重试"
- } finally {
- if (active()) repairingPage.value = undefined
- }
- }
- return { issues, repairingPage, error, generated, locate, repair }
- }
|