aegon-dangerious 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build-report/components/AiPromptGenerateDialog.vue +349 -0
- package/build-report/components/CapsuleScrollbar.vue +145 -0
- package/build-report/components/ChapterTitleScroll.vue +292 -0
- package/build-report/components/PromptField.vue +314 -0
- package/build-report/components/RecentReportsTable.vue +408 -0
- package/build-report/components/ReportChapterPreview.vue +437 -0
- package/build-report/components/ReportChapterWorkspace.vue +923 -0
- package/build-report/components/ReportCollapsibleSection.vue +132 -0
- package/build-report/components/ReportConfigPanel.vue +609 -0
- package/build-report/components/ReportGenerateSettingsPanel.vue +403 -0
- package/build-report/components/ReportGeneratingPanel.vue +908 -0
- package/build-report/components/ReportHistoryList.vue +157 -0
- package/build-report/components/ReportHistoryPanel.vue +140 -0
- package/build-report/components/ReportPromptPanel.vue +445 -0
- package/build-report/components/ReportSectionConfirmPopover.vue +232 -0
- package/build-report/components/ReportSourceFileSummary.vue +544 -0
- package/build-report/components/ReportSourcesMoreOverlay.vue +279 -0
- package/build-report/components/ReportSourcesPanel.vue +1055 -0
- package/build-report/components/ReportTemplateDetail.vue +176 -0
- package/build-report/components/ReportTemplateEditorFormPanel.vue +602 -0
- package/build-report/components/ReportTemplatePicker.vue +802 -0
- package/build-report/components/ReportTemplatePromptField.vue +314 -0
- package/build-report/components/ReportWorkflowSidebar.vue +104 -0
- package/build-report/components/reportEdit.vue +334 -0
- package/build-report/index.vue +646 -0
- package/build-report/report-list.vue +407 -0
- package/build-report/report-template-data.ts +248 -0
- package/build-report/styles/report-workflow-dialog.css +33 -0
- package/build-report/useReportWorkflow.ts +343 -0
- package/build-report/utils/report-edit-route.ts +109 -0
- package/build-report/utils/scroll.ts +361 -0
- package/package.json +12 -0
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="pageRef" class="report-generating-page" :class="{ 'is-scrollable': needsPageScroll }">
|
|
3
|
+
<div class="report-generating-page__layout">
|
|
4
|
+
<div class="report-generating-page__main">
|
|
5
|
+
<header class="report-generating-page__hero">
|
|
6
|
+
<h1 class="report-generating-page__hero-title">{{ heroTitle }}</h1>
|
|
7
|
+
</header>
|
|
8
|
+
|
|
9
|
+
<div class="report-generating-page__body">
|
|
10
|
+
<div class="report-generating-page__content">
|
|
11
|
+
<ul v-if="visibleChapters.length" class="report-generating-chapters">
|
|
12
|
+
<li v-for="chapter in visibleChapters" :key="chapter.id" class="report-generating-chapters__row">
|
|
13
|
+
<span class="report-generating-chapters__state" :class="`is-${chapter.status}`">
|
|
14
|
+
<template v-if="chapter.status === 'done'">
|
|
15
|
+
<el-icon class="report-generating-chapters__icon is-success" aria-hidden="true">
|
|
16
|
+
<CircleCheck />
|
|
17
|
+
</el-icon>
|
|
18
|
+
</template>
|
|
19
|
+
<template v-else-if="chapter.status === 'failed'">
|
|
20
|
+
<el-icon class="report-generating-chapters__icon is-failed" aria-hidden="true">
|
|
21
|
+
<Close />
|
|
22
|
+
</el-icon>
|
|
23
|
+
</template>
|
|
24
|
+
<template v-else-if="chapter.status === 'running'">
|
|
25
|
+
<el-icon class="report-generating-chapters__icon is-loading" aria-hidden="true">
|
|
26
|
+
<Loading />
|
|
27
|
+
</el-icon>
|
|
28
|
+
</template>
|
|
29
|
+
<template v-else>
|
|
30
|
+
<span class="report-generating-hourglass" aria-hidden="true">
|
|
31
|
+
<svg class="report-generating-hourglass__svg" viewBox="0 0 24 28"
|
|
32
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
33
|
+
<path class="report-generating-hourglass__frame"
|
|
34
|
+
d="M6 2h12l-5.2 9.2a1.2 1.2 0 0 0 0 1.2L18 22H6l5.2-9.6a1.2 1.2 0 0 0 0-1.2L6 2z" fill="none"
|
|
35
|
+
stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" />
|
|
36
|
+
<path class="report-generating-hourglass__sand-top" d="M8.2 4.2h7.6L12 10.8 8.2 4.2z"
|
|
37
|
+
fill="currentColor" />
|
|
38
|
+
<path class="report-generating-hourglass__sand-bottom" d="M12 17.2 15.8 23.8H8.2L12 17.2z"
|
|
39
|
+
fill="currentColor" />
|
|
40
|
+
<circle class="report-generating-hourglass__drip report-generating-hourglass__drip--1" cx="12"
|
|
41
|
+
cy="12.8" r="0.75" fill="currentColor" />
|
|
42
|
+
<circle class="report-generating-hourglass__drip report-generating-hourglass__drip--2" cx="12"
|
|
43
|
+
cy="12.8" r="0.6" fill="currentColor" />
|
|
44
|
+
<circle class="report-generating-hourglass__drip report-generating-hourglass__drip--3" cx="12"
|
|
45
|
+
cy="12.8" r="0.55" fill="currentColor" />
|
|
46
|
+
</svg>
|
|
47
|
+
</span>
|
|
48
|
+
</template>
|
|
49
|
+
</span>
|
|
50
|
+
<div class="report-generating-chapters__name">
|
|
51
|
+
<ChapterTitleScroll root-tag="span" :title="chapter.name" />
|
|
52
|
+
</div>
|
|
53
|
+
</li>
|
|
54
|
+
</ul>
|
|
55
|
+
|
|
56
|
+
<p v-if="isComplete" class="report-generating-page__summary">{{ completionSummary }}</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<footer v-if="isComplete" class="report-generating-page__footer">
|
|
61
|
+
<button type="button" class="app-btn app-btn--wide app-btn--outline"
|
|
62
|
+
:disabled="isGenerating || retrying || !failedCount" @click="retryFailedChapters">
|
|
63
|
+
重試失敗的章節
|
|
64
|
+
</button>
|
|
65
|
+
<button type="button" class="app-btn app-btn--wide" :disabled="isGenerating || retrying"
|
|
66
|
+
@click="enterEditing">
|
|
67
|
+
進入編輯
|
|
68
|
+
</button>
|
|
69
|
+
</footer>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<aside class="report-generating-page__video" aria-label="視頻播放區">
|
|
73
|
+
<div class="report-generating-page__video-slot">
|
|
74
|
+
<!-- 預留 video 嵌入位,後續可替換為 <video> 或播放器組件 -->
|
|
75
|
+
</div>
|
|
76
|
+
</aside>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</template>
|
|
80
|
+
|
|
81
|
+
<script setup lang="ts">
|
|
82
|
+
import { CircleCheck, Close, Loading } from '@element-plus/icons-vue'
|
|
83
|
+
import { ElIcon } from 'element-plus'
|
|
84
|
+
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
85
|
+
import { useRoute } from 'vue-router'
|
|
86
|
+
import {
|
|
87
|
+
MOCK_REPORT_TEMPLATES,
|
|
88
|
+
} from '../report-template-data'
|
|
89
|
+
import { useReportWorkflow } from '../useReportWorkflow'
|
|
90
|
+
import { bindViewportBodyHeight, lockDocumentScroll } from '../utils/scroll'
|
|
91
|
+
import ChapterTitleScroll from './ChapterTitleScroll.vue'
|
|
92
|
+
|
|
93
|
+
const emit = defineEmits<{
|
|
94
|
+
'enter-editing': []
|
|
95
|
+
}>()
|
|
96
|
+
|
|
97
|
+
const CHAPTER_MAX_COUNT = 100
|
|
98
|
+
|
|
99
|
+
type ChapterGenerationStatus = 'pending' | 'running' | 'done' | 'failed'
|
|
100
|
+
|
|
101
|
+
interface ChapterGenerationItem {
|
|
102
|
+
id: string
|
|
103
|
+
name: string
|
|
104
|
+
status: ChapterGenerationStatus
|
|
105
|
+
body?: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const DEFAULT_CHAPTER_NAMES: string[] = [
|
|
109
|
+
'1. 摘要與本年總結',
|
|
110
|
+
'2. 宏觀經濟與政策背景',
|
|
111
|
+
'3. 重點風險與監管關注',
|
|
112
|
+
'4. 章節名稱',
|
|
113
|
+
'5. 章節名稱',
|
|
114
|
+
'6. 章節名稱',
|
|
115
|
+
'7. 章節名稱',
|
|
116
|
+
'8. 章節名稱',
|
|
117
|
+
'9. 章節名稱',
|
|
118
|
+
'10. 章節名稱',
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
/** Demo:固定失敗章節索引(0-based),對應 7 成功 / 3 失敗 */
|
|
122
|
+
const DEMO_FAIL_INDICES = new Set<number>([2, 3, 6])
|
|
123
|
+
|
|
124
|
+
const route = useRoute()
|
|
125
|
+
|
|
126
|
+
const {
|
|
127
|
+
markGenerationSuccess,
|
|
128
|
+
selectedTemplate,
|
|
129
|
+
setSelectedTemplate,
|
|
130
|
+
setGeneratedChapterBodies,
|
|
131
|
+
loadTemplateFromSession,
|
|
132
|
+
draft,
|
|
133
|
+
} = useReportWorkflow()
|
|
134
|
+
|
|
135
|
+
const templateId = computed(() => {
|
|
136
|
+
const fromRoute = route.query.templateId
|
|
137
|
+
if (typeof fromRoute === 'string' && fromRoute) return fromRoute
|
|
138
|
+
return selectedTemplate.value?.id ?? '1'
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const pageRef = ref<HTMLElement | null>(null)
|
|
142
|
+
const needsPageScroll = ref(false)
|
|
143
|
+
let measureRafId = 0
|
|
144
|
+
let resizeObserver: ResizeObserver | null = null
|
|
145
|
+
let cleanupBodyHeight: (() => void) | null = null
|
|
146
|
+
let cleanupLock: (() => void) | null = null
|
|
147
|
+
let generationAbortController: AbortController | null = null
|
|
148
|
+
|
|
149
|
+
/** Mock 輪詢間隔(毫秒) */
|
|
150
|
+
const CHAPTER_POLL_INTERVAL_MS = 600
|
|
151
|
+
|
|
152
|
+
interface MockChapterGenerateStartResponse {
|
|
153
|
+
taskId: string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface MockChapterGeneratePollResponse {
|
|
157
|
+
success: boolean
|
|
158
|
+
status: 'running' | 'done' | 'failed'
|
|
159
|
+
progress: number
|
|
160
|
+
body?: string
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface MockChapterTask {
|
|
164
|
+
templateId: string
|
|
165
|
+
chapterIndex: number
|
|
166
|
+
chapterName: string
|
|
167
|
+
createdAt: number
|
|
168
|
+
durationMs: number
|
|
169
|
+
shouldFail: boolean
|
|
170
|
+
body: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const mockChapterTasks = new Map<string, MockChapterTask>()
|
|
174
|
+
let mockChapterTaskSeq = 0
|
|
175
|
+
|
|
176
|
+
function throwIfAborted(signal: AbortSignal) {
|
|
177
|
+
if (signal.aborted) {
|
|
178
|
+
throw new DOMException('章節生成已取消', 'AbortError')
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getMockChapterDurationMs(total: number) {
|
|
183
|
+
if (total > 30) return 1200
|
|
184
|
+
if (total > 15) return 2000
|
|
185
|
+
return 2800
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function buildMockChapterBody(chapterName: string, index: number) {
|
|
189
|
+
if (index === 0) {
|
|
190
|
+
return `本報告基於「${draft.industry || '目標行業'}」${draft.year}年度調研,從${draft.researchFocus || '核心視角'}出發,對行業風險進行系統性梳理。`
|
|
191
|
+
}
|
|
192
|
+
return `(${chapterName} 已生成,可在此編輯章節內容,並配合右側 Prompt 優化。)`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Mock API:發起章節生成(POST /report/chapter/generate/start) */
|
|
196
|
+
async function mockStartChapterGeneration(params: {
|
|
197
|
+
templateId: string
|
|
198
|
+
chapterIndex: number
|
|
199
|
+
chapterName: string
|
|
200
|
+
totalChapters: number
|
|
201
|
+
forceSuccess?: boolean
|
|
202
|
+
}): Promise<MockChapterGenerateStartResponse> {
|
|
203
|
+
await delay(180)
|
|
204
|
+
const taskId = `ch-${params.templateId}-${params.chapterIndex}-${++mockChapterTaskSeq}`
|
|
205
|
+
mockChapterTasks.set(taskId, {
|
|
206
|
+
templateId: params.templateId,
|
|
207
|
+
chapterIndex: params.chapterIndex,
|
|
208
|
+
chapterName: params.chapterName,
|
|
209
|
+
createdAt: Date.now(),
|
|
210
|
+
durationMs: getMockChapterDurationMs(params.totalChapters),
|
|
211
|
+
shouldFail: params.forceSuccess ? false : shouldFail(params.chapterIndex),
|
|
212
|
+
body: buildMockChapterBody(params.chapterName, params.chapterIndex),
|
|
213
|
+
})
|
|
214
|
+
return { taskId }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Mock API:輪詢章節生成(POST /report/chapter/generate/poll) */
|
|
218
|
+
async function mockPollChapterGeneration(taskId: string): Promise<MockChapterGeneratePollResponse> {
|
|
219
|
+
await delay(CHAPTER_POLL_INTERVAL_MS)
|
|
220
|
+
const task = mockChapterTasks.get(taskId)
|
|
221
|
+
if (!task) {
|
|
222
|
+
return { success: false, status: 'failed', progress: 0 }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const elapsed = Date.now() - task.createdAt
|
|
226
|
+
const progress = Math.min(100, Math.floor((elapsed / task.durationMs) * 100))
|
|
227
|
+
|
|
228
|
+
if (elapsed < task.durationMs) {
|
|
229
|
+
return { success: false, status: 'running', progress }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
mockChapterTasks.delete(taskId)
|
|
233
|
+
|
|
234
|
+
if (task.shouldFail) {
|
|
235
|
+
return { success: false, status: 'failed', progress: 100 }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
success: true,
|
|
240
|
+
status: 'done',
|
|
241
|
+
progress: 100,
|
|
242
|
+
body: task.body,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** 輪詢直至後端返回 success=true 或明確失敗 */
|
|
247
|
+
async function pollChapterUntilSettled(
|
|
248
|
+
taskId: string,
|
|
249
|
+
signal: AbortSignal,
|
|
250
|
+
): Promise<{ success: boolean; body?: string }> {
|
|
251
|
+
while (true) {
|
|
252
|
+
throwIfAborted(signal)
|
|
253
|
+
const res = await mockPollChapterGeneration(taskId)
|
|
254
|
+
|
|
255
|
+
if (res.status === 'running') {
|
|
256
|
+
continue
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (res.success) {
|
|
260
|
+
return { success: true, body: res.body ?? '' }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { success: false }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 單章節:發起請求 + 輪詢。
|
|
269
|
+
* 上一章 res.success === true(或失敗)後,由 runChapterGeneration 順序調用下一章。
|
|
270
|
+
*/
|
|
271
|
+
async function fetchChapterByPolling(
|
|
272
|
+
index: number,
|
|
273
|
+
chapter: ChapterGenerationItem,
|
|
274
|
+
options: { signal: AbortSignal; forceSuccess?: boolean },
|
|
275
|
+
) {
|
|
276
|
+
syncVisibleAhead(index)
|
|
277
|
+
chapter.status = 'running'
|
|
278
|
+
chapter.body = undefined
|
|
279
|
+
|
|
280
|
+
const { taskId } = await mockStartChapterGeneration({
|
|
281
|
+
templateId: templateId.value,
|
|
282
|
+
chapterIndex: index,
|
|
283
|
+
chapterName: chapter.name,
|
|
284
|
+
totalChapters: chapters.value.length,
|
|
285
|
+
forceSuccess: options.forceSuccess,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
throwIfAborted(options.signal)
|
|
289
|
+
const res = await pollChapterUntilSettled(taskId, options.signal)
|
|
290
|
+
|
|
291
|
+
chapter.status = res.success ? 'done' : 'failed'
|
|
292
|
+
chapter.body = res.success ? res.body : undefined
|
|
293
|
+
|
|
294
|
+
scheduleDetectPageScroll()
|
|
295
|
+
await delay(120)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const chapters = ref<ChapterGenerationItem[]>([])
|
|
299
|
+
const visibleCount = ref(0)
|
|
300
|
+
const isGenerating = ref(false)
|
|
301
|
+
const isComplete = ref(false)
|
|
302
|
+
const retrying = ref(false)
|
|
303
|
+
|
|
304
|
+
const visibleChapters = computed(() => chapters.value.slice(0, visibleCount.value))
|
|
305
|
+
|
|
306
|
+
const doneCount = computed(() => chapters.value.filter((item) => item.status === 'done').length)
|
|
307
|
+
const failedCount = computed(() => chapters.value.filter((item) => item.status === 'failed').length)
|
|
308
|
+
|
|
309
|
+
const completionSummary = computed(
|
|
310
|
+
() => `報告生成完畢,${doneCount.value}個章節已完成,${failedCount.value}個章節失敗`,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
const heroTitle = computed(() => (isComplete.value && !isGenerating.value ? '報告生成完畢' : '報告生成中...'))
|
|
314
|
+
|
|
315
|
+
function buildChapterNames(): string[] {
|
|
316
|
+
const fromTemplate: string[] = selectedTemplate.value?.chapters ?? []
|
|
317
|
+
|
|
318
|
+
if (fromTemplate.length) {
|
|
319
|
+
return fromTemplate.slice(0, CHAPTER_MAX_COUNT).map((name: string, index: number) => {
|
|
320
|
+
const trimmed = name.trim()
|
|
321
|
+
return /^\d+\./.test(trimmed) ? trimmed : `${index + 1}. ${trimmed}`
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return DEFAULT_CHAPTER_NAMES.slice(0, CHAPTER_MAX_COUNT)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildChapters(): ChapterGenerationItem[] {
|
|
329
|
+
return buildChapterNames().map((name, index) => ({
|
|
330
|
+
id: String(index),
|
|
331
|
+
name,
|
|
332
|
+
status: 'pending',
|
|
333
|
+
}))
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function shouldFail(index: number) {
|
|
337
|
+
return DEMO_FAIL_INDICES.has(index)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function delay(ms: number) {
|
|
341
|
+
return new Promise<void>((resolve) => {
|
|
342
|
+
window.setTimeout(resolve, ms)
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function syncVisibleAhead(currentIndex: number) {
|
|
347
|
+
visibleCount.value = Math.min(chapters.value.length, currentIndex + 3)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function detectPageScroll() {
|
|
351
|
+
await nextTick()
|
|
352
|
+
await new Promise<void>((resolve) => {
|
|
353
|
+
cancelAnimationFrame(measureRafId)
|
|
354
|
+
measureRafId = requestAnimationFrame(() => {
|
|
355
|
+
measureRafId = requestAnimationFrame(() => resolve())
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const page = pageRef.value
|
|
360
|
+
if (!page) return
|
|
361
|
+
|
|
362
|
+
const prevScrollable = page.classList.contains('is-scrollable')
|
|
363
|
+
if (!prevScrollable) {
|
|
364
|
+
page.classList.add('is-scrollable')
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const overflows = page.scrollHeight > page.clientHeight + 1
|
|
368
|
+
needsPageScroll.value = overflows
|
|
369
|
+
|
|
370
|
+
if (!overflows) {
|
|
371
|
+
page.classList.remove('is-scrollable')
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function scheduleDetectPageScroll() {
|
|
376
|
+
cancelAnimationFrame(measureRafId)
|
|
377
|
+
measureRafId = requestAnimationFrame(() => {
|
|
378
|
+
void detectPageScroll()
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function runChapterGeneration(indices: number[], signal: AbortSignal, forceSuccess = false) {
|
|
383
|
+
for (const index of indices) {
|
|
384
|
+
const chapter = chapters.value[index]
|
|
385
|
+
if (!chapter) continue
|
|
386
|
+
await fetchChapterByPolling(index, chapter, { signal, forceSuccess })
|
|
387
|
+
// 單章失敗不阻塞,繼續請求下一章
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function startGeneration() {
|
|
392
|
+
generationAbortController?.abort()
|
|
393
|
+
const controller = new AbortController()
|
|
394
|
+
generationAbortController = controller
|
|
395
|
+
|
|
396
|
+
chapters.value = buildChapters()
|
|
397
|
+
visibleCount.value = 0
|
|
398
|
+
isComplete.value = false
|
|
399
|
+
isGenerating.value = true
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const indices = chapters.value.map((_, index) => index)
|
|
403
|
+
await runChapterGeneration(indices, controller.signal)
|
|
404
|
+
|
|
405
|
+
visibleCount.value = chapters.value.length
|
|
406
|
+
isComplete.value = true
|
|
407
|
+
} catch (error) {
|
|
408
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
throw error
|
|
412
|
+
} finally {
|
|
413
|
+
isGenerating.value = false
|
|
414
|
+
scheduleDetectPageScroll()
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildChapterBody(chapter: ChapterGenerationItem, index: number) {
|
|
419
|
+
if (chapter.body) return chapter.body
|
|
420
|
+
if (chapter.status !== 'done') return ''
|
|
421
|
+
return buildMockChapterBody(chapter.name, index)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function enterEditing() {
|
|
425
|
+
if (!selectedTemplate.value) {
|
|
426
|
+
const fallback = MOCK_REPORT_TEMPLATES[0] ?? null
|
|
427
|
+
if (!fallback) return
|
|
428
|
+
setSelectedTemplate(fallback)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
setGeneratedChapterBodies(
|
|
432
|
+
chapters.value.map((chapter, index) => buildChapterBody(chapter, index)),
|
|
433
|
+
)
|
|
434
|
+
markGenerationSuccess()
|
|
435
|
+
emit('enter-editing')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function retryFailedChapters() {
|
|
439
|
+
if (isGenerating.value || retrying.value) return
|
|
440
|
+
|
|
441
|
+
const failedIndices = chapters.value
|
|
442
|
+
.map((chapter, index) => (chapter.status === 'failed' ? index : -1))
|
|
443
|
+
.filter((index) => index >= 0)
|
|
444
|
+
|
|
445
|
+
if (!failedIndices.length) return
|
|
446
|
+
|
|
447
|
+
generationAbortController?.abort()
|
|
448
|
+
const controller = new AbortController()
|
|
449
|
+
generationAbortController = controller
|
|
450
|
+
|
|
451
|
+
retrying.value = true
|
|
452
|
+
isGenerating.value = true
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
await runChapterGeneration(failedIndices, controller.signal)
|
|
456
|
+
} catch (error) {
|
|
457
|
+
if (error instanceof DOMException && error.name === 'AbortError') {
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
throw error
|
|
461
|
+
} finally {
|
|
462
|
+
isGenerating.value = false
|
|
463
|
+
retrying.value = false
|
|
464
|
+
scheduleDetectPageScroll()
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
onMounted(() => {
|
|
469
|
+
loadTemplateFromSession()
|
|
470
|
+
if (pageRef.value) {
|
|
471
|
+
cleanupBodyHeight = bindViewportBodyHeight(pageRef.value)
|
|
472
|
+
resizeObserver = new ResizeObserver(() => scheduleDetectPageScroll())
|
|
473
|
+
resizeObserver.observe(pageRef.value)
|
|
474
|
+
}
|
|
475
|
+
cleanupLock = lockDocumentScroll()
|
|
476
|
+
window.addEventListener('resize', scheduleDetectPageScroll)
|
|
477
|
+
void startGeneration()
|
|
478
|
+
scheduleDetectPageScroll()
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
onUnmounted(() => {
|
|
482
|
+
generationAbortController?.abort()
|
|
483
|
+
cancelAnimationFrame(measureRafId)
|
|
484
|
+
resizeObserver?.disconnect()
|
|
485
|
+
window.removeEventListener('resize', scheduleDetectPageScroll)
|
|
486
|
+
cleanupBodyHeight?.()
|
|
487
|
+
cleanupLock?.()
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
watch([visibleChapters, isComplete, heroTitle], () => {
|
|
491
|
+
scheduleDetectPageScroll()
|
|
492
|
+
})
|
|
493
|
+
</script>
|
|
494
|
+
|
|
495
|
+
<style scoped>
|
|
496
|
+
.report-generating-page {
|
|
497
|
+
--gen-ai-fs-hero: clamp(26px, calc(32 * 100vw / 1920), 32px);
|
|
498
|
+
--gen-ai-fw-bold: 700;
|
|
499
|
+
--report-generating-layout-width: 80%;
|
|
500
|
+
--report-generating-video-slot-width: 620px;
|
|
501
|
+
--report-generating-gap: 50px;
|
|
502
|
+
--report-generating-page-padding: 24px;
|
|
503
|
+
box-sizing: border-box;
|
|
504
|
+
min-height: 100dvh;
|
|
505
|
+
padding: var(--report-generating-page-padding);
|
|
506
|
+
background: #fff;
|
|
507
|
+
overflow-x: hidden;
|
|
508
|
+
overflow-y: hidden;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
.report-generating-page.is-scrollable {
|
|
512
|
+
overflow-y: auto;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.report-generating-page__layout {
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: flex-start;
|
|
518
|
+
justify-content: space-between;
|
|
519
|
+
gap: var(--report-generating-gap);
|
|
520
|
+
width: var(--report-generating-layout-width);
|
|
521
|
+
max-width: 100%;
|
|
522
|
+
margin: 0 auto;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
.report-generating-page__main {
|
|
526
|
+
display: flex;
|
|
527
|
+
flex: 1;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
align-items: stretch;
|
|
530
|
+
min-width: 0;
|
|
531
|
+
max-width: 100%;
|
|
532
|
+
overflow: hidden;
|
|
533
|
+
min-height: calc(100dvh - var(--report-generating-page-padding) * 2);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.report-generating-page__video {
|
|
537
|
+
flex-shrink: 0;
|
|
538
|
+
position: sticky;
|
|
539
|
+
top: var(--report-generating-page-padding);
|
|
540
|
+
align-self: flex-start;
|
|
541
|
+
display: flex;
|
|
542
|
+
justify-content: flex-end;
|
|
543
|
+
width: var(--report-generating-video-slot-width);
|
|
544
|
+
max-width: 100%;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.report-generating-page__video-slot {
|
|
548
|
+
width: var(--report-generating-video-slot-width);
|
|
549
|
+
max-width: 100%;
|
|
550
|
+
height: 400px;
|
|
551
|
+
margin-left: auto;
|
|
552
|
+
border-radius: 12px;
|
|
553
|
+
overflow: hidden;
|
|
554
|
+
background:
|
|
555
|
+
radial-gradient(ellipse 70% 55% at 28% 38%, rgba(186, 214, 255, 0.95) 0%, rgba(186, 214, 255, 0) 72%),
|
|
556
|
+
radial-gradient(ellipse 62% 48% at 72% 28%, rgba(214, 198, 255, 0.9) 0%, rgba(214, 198, 255, 0) 70%),
|
|
557
|
+
radial-gradient(ellipse 58% 52% at 52% 78%, rgba(255, 198, 220, 0.85) 0%, rgba(255, 198, 220, 0) 68%),
|
|
558
|
+
linear-gradient(135deg, #f7f9ff 0%, #f3f0ff 48%, #fff5f8 100%);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.report-generating-page__hero {
|
|
562
|
+
flex-shrink: 0;
|
|
563
|
+
text-align: left;
|
|
564
|
+
padding: 0 0 20px;
|
|
565
|
+
box-sizing: border-box;
|
|
566
|
+
background: transparent;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.report-generating-page__hero-title {
|
|
570
|
+
margin: 0;
|
|
571
|
+
font-size: var(--gen-ai-fs-hero);
|
|
572
|
+
font-weight: var(--gen-ai-fw-bold);
|
|
573
|
+
color: #1a1a1a;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.report-generating-page__body {
|
|
577
|
+
flex: 1 0 auto;
|
|
578
|
+
min-height: 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.report-generating-page__content {
|
|
582
|
+
width: 100%;
|
|
583
|
+
max-width: 100%;
|
|
584
|
+
margin: 0;
|
|
585
|
+
padding: 0;
|
|
586
|
+
box-sizing: border-box;
|
|
587
|
+
overflow: hidden;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
.report-generating-chapters {
|
|
591
|
+
margin: 0;
|
|
592
|
+
padding: 0;
|
|
593
|
+
list-style: none;
|
|
594
|
+
width: 100%;
|
|
595
|
+
max-width: 100%;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.report-generating-chapters__row {
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
justify-content: flex-start;
|
|
602
|
+
gap: 10px;
|
|
603
|
+
min-height: 40px;
|
|
604
|
+
width: 100%;
|
|
605
|
+
max-width: 100%;
|
|
606
|
+
font-size: 14px;
|
|
607
|
+
font-weight: 500;
|
|
608
|
+
color: #303133;
|
|
609
|
+
overflow: hidden;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.report-generating-chapters__state {
|
|
613
|
+
display: inline-flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
justify-content: center;
|
|
616
|
+
flex-shrink: 0;
|
|
617
|
+
width: 22px;
|
|
618
|
+
min-height: 40px;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
.report-generating-chapters__name {
|
|
622
|
+
flex: 1;
|
|
623
|
+
min-width: 0;
|
|
624
|
+
overflow: hidden;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll) {
|
|
628
|
+
display: block;
|
|
629
|
+
width: 100%;
|
|
630
|
+
max-width: 100%;
|
|
631
|
+
height: 40px;
|
|
632
|
+
line-height: 40px;
|
|
633
|
+
font-size: 14px;
|
|
634
|
+
font-weight: 500;
|
|
635
|
+
color: #303133;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll__viewport) {
|
|
639
|
+
height: 40px;
|
|
640
|
+
line-height: 40px;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll__text) {
|
|
644
|
+
color: #303133;
|
|
645
|
+
font-weight: 500;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll.is-truncatable) {
|
|
649
|
+
cursor: pointer;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll.is-truncatable.is-dragging) {
|
|
653
|
+
cursor: grabbing;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.report-generating-chapters__name :deep(.chapter-title-scroll__ellipsis) {
|
|
657
|
+
background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, #fff 45%);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.report-generating-chapters__icon {
|
|
661
|
+
font-size: 18px;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.report-generating-chapters__icon.is-success {
|
|
665
|
+
color: #52c41a;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
.report-generating-chapters__icon.is-failed {
|
|
669
|
+
color: #c53355;
|
|
670
|
+
font-weight: 700;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.report-generating-chapters__icon.is-loading {
|
|
674
|
+
color: #409eff;
|
|
675
|
+
animation: report-generating-spin 1s linear infinite;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.report-generating-hourglass {
|
|
679
|
+
display: inline-flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
justify-content: center;
|
|
682
|
+
width: 18px;
|
|
683
|
+
height: 22px;
|
|
684
|
+
color: #909399;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.report-generating-hourglass__svg {
|
|
688
|
+
width: 100%;
|
|
689
|
+
height: 100%;
|
|
690
|
+
overflow: visible;
|
|
691
|
+
animation: report-generating-hourglass-flip 2.8s ease-in-out infinite;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.report-generating-hourglass__sand-top {
|
|
695
|
+
transform-box: fill-box;
|
|
696
|
+
transform-origin: center top;
|
|
697
|
+
animation: report-generating-sand-top 2.8s ease-in-out infinite;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
.report-generating-hourglass__sand-bottom {
|
|
701
|
+
transform-box: fill-box;
|
|
702
|
+
transform-origin: center bottom;
|
|
703
|
+
animation: report-generating-sand-bottom 2.8s ease-in-out infinite;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.report-generating-hourglass__drip {
|
|
707
|
+
opacity: 0;
|
|
708
|
+
animation: report-generating-sand-drip 1.1s linear infinite;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.report-generating-hourglass__drip--2 {
|
|
712
|
+
animation-delay: 0.35s;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
.report-generating-hourglass__drip--3 {
|
|
716
|
+
animation-delay: 0.7s;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
@keyframes report-generating-hourglass-flip {
|
|
720
|
+
|
|
721
|
+
0%,
|
|
722
|
+
42% {
|
|
723
|
+
transform: rotate(0deg);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
50%,
|
|
727
|
+
92% {
|
|
728
|
+
transform: rotate(180deg);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
100% {
|
|
732
|
+
transform: rotate(360deg);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
@keyframes report-generating-sand-top {
|
|
737
|
+
|
|
738
|
+
0%,
|
|
739
|
+
8% {
|
|
740
|
+
transform: scaleY(1);
|
|
741
|
+
opacity: 0.9;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
40%,
|
|
745
|
+
50% {
|
|
746
|
+
transform: scaleY(0.12);
|
|
747
|
+
opacity: 0.35;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
58%,
|
|
751
|
+
100% {
|
|
752
|
+
transform: scaleY(1);
|
|
753
|
+
opacity: 0.9;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
@keyframes report-generating-sand-bottom {
|
|
758
|
+
|
|
759
|
+
0%,
|
|
760
|
+
8% {
|
|
761
|
+
transform: scaleY(0.12);
|
|
762
|
+
opacity: 0.35;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
40%,
|
|
766
|
+
50% {
|
|
767
|
+
transform: scaleY(1);
|
|
768
|
+
opacity: 0.9;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
58%,
|
|
772
|
+
100% {
|
|
773
|
+
transform: scaleY(0.12);
|
|
774
|
+
opacity: 0.35;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
@keyframes report-generating-sand-drip {
|
|
779
|
+
0% {
|
|
780
|
+
transform: translateY(0);
|
|
781
|
+
opacity: 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
12% {
|
|
785
|
+
opacity: 0.95;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
88% {
|
|
789
|
+
opacity: 0.85;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
100% {
|
|
793
|
+
transform: translateY(6px);
|
|
794
|
+
opacity: 0;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.report-generating-chapters__state.is-running .report-generating-chapters__icon.is-loading {
|
|
799
|
+
color: #409eff;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.report-generating-page__footer {
|
|
803
|
+
flex-shrink: 0;
|
|
804
|
+
display: flex;
|
|
805
|
+
align-items: center;
|
|
806
|
+
justify-content: center;
|
|
807
|
+
gap: 16px;
|
|
808
|
+
width: 100%;
|
|
809
|
+
margin-top: auto;
|
|
810
|
+
padding: 32px 0 0;
|
|
811
|
+
background: transparent;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
@keyframes report-generating-spin {
|
|
815
|
+
from {
|
|
816
|
+
transform: rotate(0deg);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
to {
|
|
820
|
+
transform: rotate(360deg);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.report-generating-page__summary {
|
|
825
|
+
margin: 28px 0 0;
|
|
826
|
+
font-size: 15px;
|
|
827
|
+
font-weight: 600;
|
|
828
|
+
color: #303133;
|
|
829
|
+
text-align: left;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
.app-btn {
|
|
833
|
+
display: inline-flex;
|
|
834
|
+
align-items: center;
|
|
835
|
+
justify-content: center;
|
|
836
|
+
box-sizing: border-box;
|
|
837
|
+
width: 160px;
|
|
838
|
+
height: 39px;
|
|
839
|
+
min-width: 160px;
|
|
840
|
+
padding: 0;
|
|
841
|
+
border: 1px solid #c53355;
|
|
842
|
+
border-radius: 4px;
|
|
843
|
+
background: #c53355;
|
|
844
|
+
color: #fff;
|
|
845
|
+
font-size: 14px;
|
|
846
|
+
font-weight: 600;
|
|
847
|
+
line-height: 1;
|
|
848
|
+
cursor: pointer;
|
|
849
|
+
transition: all 0.2s ease;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
.app-btn:hover:not(:disabled) {
|
|
853
|
+
background: #fff;
|
|
854
|
+
color: #c53355;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
.app-btn:disabled {
|
|
858
|
+
opacity: 0.45;
|
|
859
|
+
cursor: not-allowed;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.app-btn--wide {
|
|
863
|
+
width: 160px;
|
|
864
|
+
min-width: 160px;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.app-btn--outline {
|
|
868
|
+
background: #fff;
|
|
869
|
+
color: #c53355;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
.app-btn--outline:hover:not(:disabled) {
|
|
873
|
+
background: #c53355;
|
|
874
|
+
color: #fff;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
@media (max-width: 960px) {
|
|
878
|
+
.report-generating-page {
|
|
879
|
+
--report-generating-page-padding: 16px;
|
|
880
|
+
--report-generating-layout-width: 100%;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.report-generating-page__layout {
|
|
884
|
+
flex-direction: column;
|
|
885
|
+
gap: 24px;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
.report-generating-page__video {
|
|
889
|
+
position: relative;
|
|
890
|
+
top: auto;
|
|
891
|
+
order: -1;
|
|
892
|
+
width: 100%;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.report-generating-page__video-slot {
|
|
896
|
+
width: min(var(--report-generating-video-slot-width), 100%);
|
|
897
|
+
margin-left: auto;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.report-generating-page__main {
|
|
901
|
+
width: 100%;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.report-generating-page__footer {
|
|
905
|
+
justify-content: center;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
</style>
|