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.
Files changed (32) hide show
  1. package/build-report/components/AiPromptGenerateDialog.vue +349 -0
  2. package/build-report/components/CapsuleScrollbar.vue +145 -0
  3. package/build-report/components/ChapterTitleScroll.vue +292 -0
  4. package/build-report/components/PromptField.vue +314 -0
  5. package/build-report/components/RecentReportsTable.vue +408 -0
  6. package/build-report/components/ReportChapterPreview.vue +437 -0
  7. package/build-report/components/ReportChapterWorkspace.vue +923 -0
  8. package/build-report/components/ReportCollapsibleSection.vue +132 -0
  9. package/build-report/components/ReportConfigPanel.vue +609 -0
  10. package/build-report/components/ReportGenerateSettingsPanel.vue +403 -0
  11. package/build-report/components/ReportGeneratingPanel.vue +908 -0
  12. package/build-report/components/ReportHistoryList.vue +157 -0
  13. package/build-report/components/ReportHistoryPanel.vue +140 -0
  14. package/build-report/components/ReportPromptPanel.vue +445 -0
  15. package/build-report/components/ReportSectionConfirmPopover.vue +232 -0
  16. package/build-report/components/ReportSourceFileSummary.vue +544 -0
  17. package/build-report/components/ReportSourcesMoreOverlay.vue +279 -0
  18. package/build-report/components/ReportSourcesPanel.vue +1055 -0
  19. package/build-report/components/ReportTemplateDetail.vue +176 -0
  20. package/build-report/components/ReportTemplateEditorFormPanel.vue +602 -0
  21. package/build-report/components/ReportTemplatePicker.vue +802 -0
  22. package/build-report/components/ReportTemplatePromptField.vue +314 -0
  23. package/build-report/components/ReportWorkflowSidebar.vue +104 -0
  24. package/build-report/components/reportEdit.vue +334 -0
  25. package/build-report/index.vue +646 -0
  26. package/build-report/report-list.vue +407 -0
  27. package/build-report/report-template-data.ts +248 -0
  28. package/build-report/styles/report-workflow-dialog.css +33 -0
  29. package/build-report/useReportWorkflow.ts +343 -0
  30. package/build-report/utils/report-edit-route.ts +109 -0
  31. package/build-report/utils/scroll.ts +361 -0
  32. 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>