|
@@ -19,7 +19,7 @@
|
|
|
>{{ label }}
|
|
>{{ label }}
|
|
|
</li>
|
|
</li>
|
|
|
</ol>
|
|
</ol>
|
|
|
- <main>
|
|
|
|
|
|
|
+ <main ref="mainContent">
|
|
|
<section v-if="step === 1" class="setup">
|
|
<section v-if="step === 1" class="setup">
|
|
|
<h3>这节课准备怎么上?</h3>
|
|
<h3>这节课准备怎么上?</h3>
|
|
|
<label>课题来源<select :value="preferences.lessonTopic ? 'custom' : 'current'" :disabled="outlineLoading" @change="changeTopicSource">
|
|
<label>课题来源<select :value="preferences.lessonTopic ? 'custom' : 'current'" :disabled="outlineLoading" @change="changeTopicSource">
|
|
@@ -178,33 +178,15 @@
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
- <section v-else class="generation" aria-live="polite">
|
|
|
|
|
- <template v-if="generationError"
|
|
|
|
|
|
|
+ <section v-else class="generation" :class="{ 'showing-preview': generationError && completionSource && showCompletionContent }" aria-live="polite">
|
|
|
|
|
+ <AiCoursewareDraftPreview v-if="generationError && completionSource && showCompletionContent"
|
|
|
|
|
+ :outline="completionSource" :template-id="templateId" :message="generationError" />
|
|
|
|
|
+ <template v-else-if="generationError"
|
|
|
><span class="error-icon">!</span>
|
|
><span class="error-icon">!</span>
|
|
|
<h3>{{ completionSource ? `已保留 ${completionSource.pages.length} 页,部分内容待完善` : task ? "课件进度暂时无法继续" : "这次生成未完成" }}</h3>
|
|
<h3>{{ completionSource ? `已保留 ${completionSource.pages.length} 页,部分内容待完善` : task ? "课件进度暂时无法继续" : "这次生成未完成" }}</h3>
|
|
|
<p>{{ generationError }}</p>
|
|
<p>{{ generationError }}</p>
|
|
|
- <p class="hint">{{ completionSource ? /配置不完整|配置关闭|配置不受支持/.test(generationError) ? '可以先查看已生成内容;配图服务恢复后再点击“继续自动完善”。' : '可以先查看已生成内容,再点击“继续自动完善”。系统会沿用当前结果处理,当前课件没有被替换。' : '已保留确认的大纲,当前课件没有被替换。' }}</p>
|
|
|
|
|
- <div v-if="completionSource && showCompletionContent" class="completion-content">
|
|
|
|
|
- <h4>已生成内容</h4>
|
|
|
|
|
- <p>这里可查看各页文字和教学备注。</p>
|
|
|
|
|
- <details v-for="page in completionSource.pages" :key="page.pageNo">
|
|
|
|
|
- <summary>第 {{ page.pageNo }} 页 · {{ page.title }}</summary>
|
|
|
|
|
- <p v-if="page.studentContent?.headline">{{ page.studentContent.headline }}</p>
|
|
|
|
|
- <p v-if="page.classroomAction?.studentTask">{{ page.classroomAction.studentTask }}</p>
|
|
|
|
|
- <ul><li v-for="(point, index) in (page.studentContent?.points || page.bullets || [])" :key="index">{{ point }}</li></ul>
|
|
|
|
|
- <ol><li v-for="(item, index) in page.studentContent?.steps" :key="index">{{ item }}</li></ol>
|
|
|
|
|
- <p v-if="page.studentContent?.example">示例:{{ page.studentContent.example }}</p>
|
|
|
|
|
- <div v-for="(group, index) in page.studentContent?.comparisonGroups" :key="`group-${index}`">
|
|
|
|
|
- <p>{{ group.label }}</p>
|
|
|
|
|
- <ul><li v-for="(point, pointIndex) in group.points" :key="pointIndex">{{ point }}</li></ul>
|
|
|
|
|
- </div>
|
|
|
|
|
- <p v-if="page.interaction?.question">课堂提问:{{ page.interaction.question }}</p>
|
|
|
|
|
- <ul><li v-for="(option, index) in page.interaction?.options" :key="index">{{ option }}</li></ul>
|
|
|
|
|
- <p v-if="page.classroomAction?.modelExample" class="completion-notes">教学示范:{{ page.classroomAction.modelExample }}</p>
|
|
|
|
|
- <p v-if="page.classroomAction?.storyScript" class="completion-notes">故事讲稿:{{ page.classroomAction.storyScript }}</p>
|
|
|
|
|
- <p v-if="page.speakerNotes || page.teacherTips" class="completion-notes">教学备注:{{ page.speakerNotes || page.teacherTips }}</p>
|
|
|
|
|
- </details>
|
|
|
|
|
- </div></template
|
|
|
|
|
|
|
+ <p class="hint">{{ completionSource ? '可以查看逐页预览,或打开草稿编辑并补图。自动完善仍未成功时,已生成的页面仍可使用。' : '已保留确认的大纲,当前课件没有被替换。' }}</p>
|
|
|
|
|
+ </template
|
|
|
><template v-else
|
|
><template v-else
|
|
|
><span class="spinner"></span>
|
|
><span class="spinner"></span>
|
|
|
<h3>正在生成课件</h3>
|
|
<h3>正在生成课件</h3>
|
|
@@ -229,9 +211,10 @@
|
|
|
</div>
|
|
</div>
|
|
|
<div v-else-if="generationError">
|
|
<div v-else-if="generationError">
|
|
|
<Button v-if="!transferring" @click="backToSettings">修改需求</Button>
|
|
<Button v-if="!transferring" @click="backToSettings">修改需求</Button>
|
|
|
- <Button v-if="completionSource" :aria-expanded="showCompletionContent" @click="showCompletionContent = !showCompletionContent">{{ showCompletionContent ? "收起已生成内容" : "查看已生成内容" }}</Button>
|
|
|
|
|
- <Button v-if="!task" @click="step = 2">返回修改大纲</Button
|
|
|
|
|
- ><Button type="primary" :disabled="transferring" @click="retry">{{ completionSource ? "继续自动完善" : layoutRepairSource ? "精简并重新排版" : task ? "继续查看课件" : "重试生成" }}</Button>
|
|
|
|
|
|
|
+ <Button v-if="completionSource" :disabled="transferring" :aria-expanded="showCompletionContent" @click="showCompletionContent = !showCompletionContent">{{ showCompletionContent ? "收起预览" : "查看已生成内容" }}</Button>
|
|
|
|
|
+ <Button v-if="!task && !completionSource" @click="step = 2">返回修改大纲</Button>
|
|
|
|
|
+ <Button :type="completionSource ? 'default' : 'primary'" :disabled="transferring" @click="retry">{{ layoutRepairSource && layoutRepairFeedback ? "精简并重新排版" : completionSource ? "继续自动完善" : task ? "继续查看课件" : "重试生成" }}</Button>
|
|
|
|
|
+ <Button v-if="completionSource" type="primary" :disabled="transferring" @click="openCompletionDraft">{{ transferring ? "正在打开…" : "打开草稿编辑" }}</Button>
|
|
|
</div>
|
|
</div>
|
|
|
</footer>
|
|
</footer>
|
|
|
</div>
|
|
</div>
|
|
@@ -246,6 +229,7 @@ import Draggable from "vuedraggable"
|
|
|
import { nanoid } from "nanoid"
|
|
import { nanoid } from "nanoid"
|
|
|
import Modal from "@/components/Modal.vue"
|
|
import Modal from "@/components/Modal.vue"
|
|
|
import Button from "@/components/Button.vue"
|
|
import Button from "@/components/Button.vue"
|
|
|
|
|
+import AiCoursewareDraftPreview from './AiCoursewareDraftPreview.vue'
|
|
|
import queryParams from "@/queryParams"
|
|
import queryParams from "@/queryParams"
|
|
|
import usePptWork from "@/store/pptWork"
|
|
import usePptWork from "@/store/pptWork"
|
|
|
import { useMainStore, useSlidesStore, useSnapshotStore } from "@/store"
|
|
import { useMainStore, useSlidesStore, useSnapshotStore } from "@/store"
|
|
@@ -321,8 +305,14 @@ const task = ref<AiCoursewareTaskInfo>(),
|
|
|
const layoutRepairSource = ref<AiCoursewareOutline>()
|
|
const layoutRepairSource = ref<AiCoursewareOutline>()
|
|
|
const layoutRepairFeedback = ref('')
|
|
const layoutRepairFeedback = ref('')
|
|
|
const completionSource = ref<AiCoursewareOutline>()
|
|
const completionSource = ref<AiCoursewareOutline>()
|
|
|
|
|
+const completionReviewMessage = ref('')
|
|
|
|
|
+const mainContent = ref<HTMLElement>()
|
|
|
const showCompletionContent = ref(false)
|
|
const showCompletionContent = ref(false)
|
|
|
-watch(completionSource, () => { showCompletionContent.value = false })
|
|
|
|
|
|
|
+watch(completionSource, value => {
|
|
|
|
|
+ showCompletionContent.value = !!value
|
|
|
|
|
+ if (!value) completionReviewMessage.value = ''
|
|
|
|
|
+ else void nextTick(() => { if (mainContent.value) mainContent.value.scrollTop = 0 })
|
|
|
|
|
+})
|
|
|
const outlineTask = ref<AiCoursewareTaskInfo>(), outlineTaskSettings = ref('')
|
|
const outlineTask = ref<AiCoursewareTaskInfo>(), outlineTaskSettings = ref('')
|
|
|
let pollTimer: ReturnType<typeof setTimeout> | undefined,
|
|
let pollTimer: ReturnType<typeof setTimeout> | undefined,
|
|
|
version = 0,
|
|
version = 0,
|
|
@@ -407,6 +397,7 @@ function remember() {
|
|
|
pendingOutlineSettings = outlineTaskSettings.value,
|
|
pendingOutlineSettings = outlineTaskSettings.value,
|
|
|
pendingCompletion = completionSource.value ? clone(completionSource.value) : undefined,
|
|
pendingCompletion = completionSource.value ? clone(completionSource.value) : undefined,
|
|
|
completionMessage = generationError.value,
|
|
completionMessage = generationError.value,
|
|
|
|
|
+ reviewMessage = completionReviewMessage.value,
|
|
|
savedSettings = outlineSettings.value
|
|
savedSettings = outlineSettings.value
|
|
|
if (!id) return
|
|
if (!id) return
|
|
|
saveQueue = saveQueue
|
|
saveQueue = saveQueue
|
|
@@ -422,6 +413,7 @@ function remember() {
|
|
|
outlineTaskSettings: pendingOutlineSettings,
|
|
outlineTaskSettings: pendingOutlineSettings,
|
|
|
completionSource: pendingCompletion,
|
|
completionSource: pendingCompletion,
|
|
|
completionMessage,
|
|
completionMessage,
|
|
|
|
|
+ reviewMessage,
|
|
|
outlineSettings: savedSettings
|
|
outlineSettings: savedSettings
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
@@ -469,6 +461,7 @@ async function restore() {
|
|
|
} else if (saved.completionSource) {
|
|
} else if (saved.completionSource) {
|
|
|
confirmed.value = currentPlan()
|
|
confirmed.value = currentPlan()
|
|
|
completionSource.value = clone(saved.completionSource)
|
|
completionSource.value = clone(saved.completionSource)
|
|
|
|
|
+ completionReviewMessage.value = saved.reviewMessage || ''
|
|
|
generationError.value = saved.completionMessage || '课件尚未全部完成,可接着完成。'
|
|
generationError.value = saved.completionMessage || '课件尚未全部完成,可接着完成。'
|
|
|
step.value = 3
|
|
step.value = 3
|
|
|
if (saved.taskId) {
|
|
if (saved.taskId) {
|
|
@@ -673,6 +666,8 @@ async function startGeneration() {
|
|
|
if (props.visible && task.value) await pollTask()
|
|
if (props.visible && task.value) await pollTask()
|
|
|
}
|
|
}
|
|
|
async function retry() {
|
|
async function retry() {
|
|
|
|
|
+ if (transferring.value) return
|
|
|
|
|
+ if (layoutRepairSource.value && layoutRepairFeedback.value) { await repairBlockedLayout(); return }
|
|
|
if (completionSource.value) {
|
|
if (completionSource.value) {
|
|
|
if (task.value && (['PENDING', 'RUNNING'].includes(task.value.status)
|
|
if (task.value && (['PENDING', 'RUNNING'].includes(task.value.status)
|
|
|
|| isAdvisoryCompletion(generationError.value.split(';已保留')[0]))) { generationError.value = ''; await pollTask() }
|
|
|| isAdvisoryCompletion(generationError.value.split(';已保留')[0]))) { generationError.value = ''; await pollTask() }
|
|
@@ -783,23 +778,38 @@ async function pollTask() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-async function enterEditor(result: AiCoursewareGenerateResult, active: () => boolean) {
|
|
|
|
|
|
|
+async function openCompletionDraft() {
|
|
|
|
|
+ if (transferring.value || !completionSource.value) return
|
|
|
|
|
+ const requestVersion = ++version
|
|
|
|
|
+ stopPolling()
|
|
|
|
|
+ const active = () => requestVersion === version && props.visible
|
|
|
|
|
+ transferring.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ await enterEditor({ outline: clone(completionSource.value), context: {},
|
|
|
|
|
+ blockingMessage: generationError.value, reviewMessage: completionReviewMessage.value }, active, true)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (active()) generationError.value = error instanceof Error ? error.message : '暂时无法打开草稿,已保留生成内容。'
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (requestVersion === version) transferring.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+async function enterEditor(result: AiCoursewareGenerateResult, active: () => boolean, allowIncompleteDraft = false) {
|
|
|
if (!result?.outline?.pages?.length) throw new Error("尚未获得课件内容,请继续查看")
|
|
if (!result?.outline?.pages?.length) throw new Error("尚未获得课件内容,请继续查看")
|
|
|
if (!matchesRequestedTopic(result.outline)) throw new Error('生成结果与本次教材或课题不符,原课件已保留,请重新生成大纲。')
|
|
if (!matchesRequestedTopic(result.outline)) throw new Error('生成结果与本次教材或课题不符,原课件已保留,请重新生成大纲。')
|
|
|
await assertAiPptClientCurrent()
|
|
await assertAiPptClientCurrent()
|
|
|
if (!active()) return
|
|
if (!active()) return
|
|
|
- if (task.value?.status === 'DRAFT') {
|
|
|
|
|
|
|
+ if (task.value?.status === 'DRAFT' && !allowIncompleteDraft) {
|
|
|
if (isAdvisoryCompletion(result.blockingMessage)) {
|
|
if (isAdvisoryCompletion(result.blockingMessage)) {
|
|
|
// 历史任务保留DRAFT状态时无需再次调用生成;让正常渲染与缺图校验决定能否进入。
|
|
// 历史任务保留DRAFT状态时无需再次调用生成;让正常渲染与缺图校验决定能否进入。
|
|
|
result = { ...result, reviewMessage: [result.reviewMessage, result.blockingMessage].filter(Boolean).join(';'), blockingMessage: '' }
|
|
result = { ...result, reviewMessage: [result.reviewMessage, result.blockingMessage].filter(Boolean).join(';'), blockingMessage: '' }
|
|
|
} else {
|
|
} else {
|
|
|
completionSource.value = clone(result.outline)
|
|
completionSource.value = clone(result.outline)
|
|
|
|
|
+ completionReviewMessage.value = result.reviewMessage || ''
|
|
|
generationError.value = coursewareCompletionMessage(result, task.value.message)
|
|
generationError.value = coursewareCompletionMessage(result, task.value.message)
|
|
|
remember()
|
|
remember()
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- completionSource.value = undefined
|
|
|
|
|
layoutRepairSource.value = clone(result.outline)
|
|
layoutRepairSource.value = clone(result.outline)
|
|
|
layoutRepairFeedback.value = ''
|
|
layoutRepairFeedback.value = ''
|
|
|
let converted: ReturnType<typeof aiCoursewareToSlides>
|
|
let converted: ReturnType<typeof aiCoursewareToSlides>
|
|
@@ -830,21 +840,25 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
|
|
|
const imageIssues = blockingImageIssues(issues)
|
|
const imageIssues = blockingImageIssues(issues)
|
|
|
if (imageIssues.length) {
|
|
if (imageIssues.length) {
|
|
|
if (!active()) return
|
|
if (!active()) return
|
|
|
- completionSource.value = clone(result.outline)
|
|
|
|
|
|
|
+ result = { ...result, outline: clone(result.outline) }
|
|
|
// 地址存在但实际加载失败的图片不能在续作时误当作成功配图复用。
|
|
// 地址存在但实际加载失败的图片不能在续作时误当作成功配图复用。
|
|
|
imageIssues.filter(issue => issue.code === 'resource_unavailable').forEach(issue => {
|
|
imageIssues.filter(issue => issue.code === 'resource_unavailable').forEach(issue => {
|
|
|
- const page = completionSource.value?.pages.find(page => page.pageNo === issue.pageNo)
|
|
|
|
|
|
|
+ const page = result.outline.pages.find(page => page.pageNo === issue.pageNo)
|
|
|
if (page) {
|
|
if (page) {
|
|
|
const failedUrl = page.image?.url
|
|
const failedUrl = page.image?.url
|
|
|
page.image = undefined; page.imageStatus = 'generation_failed'
|
|
page.image = undefined; page.imageStatus = 'generation_failed'
|
|
|
page.resourceBindings = page.resourceBindings?.filter(binding => binding.resourceType !== 'IMG' || binding.contentUrl !== failedUrl)
|
|
page.resourceBindings = page.resourceBindings?.filter(binding => binding.resourceType !== 'IMG' || binding.contentUrl !== failedUrl)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- generationError.value = `第${[...new Set(imageIssues.map(issue => issue.pageNo))].join('、')}页配图尚未完成,已保留生成内容。`
|
|
|
|
|
- remember()
|
|
|
|
|
- return
|
|
|
|
|
|
|
+ if (!allowIncompleteDraft) {
|
|
|
|
|
+ completionSource.value = result.outline
|
|
|
|
|
+ completionReviewMessage.value = result.reviewMessage || ''
|
|
|
|
|
+ generationError.value = `第${[...new Set(imageIssues.map(issue => issue.pageNo))].join('、')}页配图尚未完成,已保留生成内容。`
|
|
|
|
|
+ remember()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- const draft = issues.some(issue => issue.severity === 'error')
|
|
|
|
|
|
|
+ const draft = allowIncompleteDraft || issues.some(issue => issue.severity === 'error')
|
|
|
if (!active()) return
|
|
if (!active()) return
|
|
|
if (
|
|
if (
|
|
|
replacementSnapshot !== editorSnapshot() &&
|
|
replacementSnapshot !== editorSnapshot() &&
|
|
@@ -885,7 +899,7 @@ async function enterEditor(result: AiCoursewareGenerateResult, active: () => boo
|
|
|
remember()
|
|
remember()
|
|
|
emit("generated", issues, clone({ ...result.outline, templateId: templateId.value }), clone(preferences))
|
|
emit("generated", issues, clone({ ...result.outline, templateId: templateId.value }), clone(preferences))
|
|
|
emit("update:visible", false)
|
|
emit("update:visible", false)
|
|
|
- if (draft) ElMessage.warning("草稿已打开,内容仍需完善;可查看问题并点击“完善整课”。")
|
|
|
|
|
|
|
+ if (draft) ElMessage.warning("草稿已打开,待完善问题已保留。可逐页编辑、补图,完成后保存课件。")
|
|
|
else ElMessage.success("课件已生成,可直接编辑,完成后点击“保存课件”")
|
|
else ElMessage.success("课件已生成,可直接编辑,完成后点击“保存课件”")
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
if (changed) {
|
|
if (changed) {
|
|
@@ -1153,24 +1167,7 @@ button:focus-visible {
|
|
|
.generation p {
|
|
.generation p {
|
|
|
max-width: 650px;
|
|
max-width: 650px;
|
|
|
}
|
|
}
|
|
|
-.completion-content {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- max-width: 850px;
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
- padding: 18px;
|
|
|
|
|
- text-align: left;
|
|
|
|
|
- background: #fff;
|
|
|
|
|
- border: 1px solid #e2e9f1;
|
|
|
|
|
- border-radius: 8px;
|
|
|
|
|
-}
|
|
|
|
|
-.completion-content details { padding: 12px 0; border-top: 1px solid #e2e9f1; }
|
|
|
|
|
-.completion-content summary { cursor: pointer; font-weight: 600; }
|
|
|
|
|
-.completion-content p { max-width: none; white-space: pre-wrap; }
|
|
|
|
|
-.completion-content ul, .completion-content ol { padding-left: 22px; }
|
|
|
|
|
-.completion-content ul { list-style: disc; }
|
|
|
|
|
-.completion-content ol { list-style: decimal; }
|
|
|
|
|
-.completion-content li { margin: 7px 0; }
|
|
|
|
|
-.completion-notes { color: #65758a; }
|
|
|
|
|
|
|
+.generation.showing-preview { justify-content: flex-start; padding: 16px; }
|
|
|
progress {
|
|
progress {
|
|
|
width: 320px;
|
|
width: 320px;
|
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
@@ -1218,6 +1215,8 @@ footer {
|
|
|
}
|
|
}
|
|
|
footer > div {
|
|
footer > div {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
gap: 12px;
|
|
gap: 12px;
|
|
|
}
|
|
}
|
|
|
@media (max-width: 740px) {
|
|
@media (max-width: 740px) {
|