aegon-lv97 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 (78) hide show
  1. package/gen-ai/build-report/chartStreamRefs.ts +227 -0
  2. package/gen-ai/build-report/coherence-check/index.vue +559 -0
  3. package/gen-ai/build-report/components/ReportChapterWorkspace.vue +4571 -0
  4. package/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  5. package/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  6. package/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  7. package/gen-ai/build-report/components/ReportPromptPanel.vue +625 -0
  8. package/gen-ai/build-report/components/ReportSourceFileSummary.vue +665 -0
  9. package/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  10. package/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  11. package/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  12. package/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  13. package/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  14. package/gen-ai/build-report/components/paramsSetting.vue +886 -0
  15. package/gen-ai/build-report/components/reportEdit.vue +2283 -0
  16. package/gen-ai/build-report/index.vue +856 -0
  17. package/gen-ai/build-report/report-domain.ts +1133 -0
  18. package/gen-ai/build-report/report-template-data.ts +295 -0
  19. package/gen-ai/build-report/useReportWorkflow.ts +1386 -0
  20. package/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  21. package/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  22. package/gen-ai/common/assets/NotoSans.woff2 +0 -0
  23. package/gen-ai/common/assets/fonts.css +278 -0
  24. package/gen-ai/common/assets/pagination.css +163 -0
  25. package/gen-ai/common/directives/btnLoading.ts +455 -0
  26. package/gen-ai/common/directives/debounce.ts +98 -0
  27. package/gen-ai/common/directives/index.ts +30 -0
  28. package/gen-ai/common/en.md +417 -0
  29. package/gen-ai/common/index.ts +36 -0
  30. package/gen-ai/common/types/component-expose.ts +9 -0
  31. package/gen-ai/common/utils/api-res-status.ts +7 -0
  32. package/gen-ai/common/utils/navigation.ts +59 -0
  33. package/gen-ai/common/utils/remote-request.ts +45 -0
  34. package/gen-ai/common/utils/scroll.ts +577 -0
  35. package/gen-ai/components/AIExport/docx-export.ts +950 -0
  36. package/gen-ai/components/AIExport/docxExport.ts +2566 -0
  37. package/gen-ai/components/AIExport/index.ts +52 -0
  38. package/gen-ai/components/AIExport/parseMarkdownHtml.ts +653 -0
  39. package/gen-ai/components/CapsuleScrollbar.vue +149 -0
  40. package/gen-ai/components/ChapterTitleScroll.vue +468 -0
  41. package/gen-ai/components/EntryHeroNavCards.vue +379 -0
  42. package/gen-ai/components/GenAiPagination.vue +52 -0
  43. package/gen-ai/components/ModelCard.vue +1114 -0
  44. package/gen-ai/components/Preview/FilePreviewPage.vue +949 -0
  45. package/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  46. package/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  47. package/gen-ai/components/Preview/index.ts +11 -0
  48. package/gen-ai/components/Preview/preview.ts +724 -0
  49. package/gen-ai/components/PromptField.vue +505 -0
  50. package/gen-ai/components/RecentReportsTable.vue +974 -0
  51. package/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  52. package/gen-ai/components/ReportHistoryList.vue +637 -0
  53. package/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  54. package/gen-ai/components/editor/ChapterTiptapEditor.vue +3718 -0
  55. package/gen-ai/components/editor/StructureBlockView.vue +441 -0
  56. package/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  57. package/gen-ai/components/editor/TypewriterDocEditor.vue +422 -0
  58. package/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  59. package/gen-ai/components/editor/index.ts +28 -0
  60. package/gen-ai/components/editor/parseMarkdownHtml.ts +754 -0
  61. package/gen-ai/components/editor/reviewCitationTip.ts +163 -0
  62. package/gen-ai/gen-entry/api/index.ts +32 -0
  63. package/gen-ai/gen-entry/index.vue +287 -0
  64. package/gen-ai/gen-entry/types/index.ts +4 -0
  65. package/gen-ai/i.mark +2 -0
  66. package/gen-ai/management-center/index.vue +1815 -0
  67. package/gen-ai/management-center/mockData.ts +248 -0
  68. package/gen-ai/management-center/types/index.ts +37 -0
  69. package/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  70. package/gen-ai/model-manager/api/index.ts +32 -0
  71. package/gen-ai/model-manager/api/mockApi.ts +33 -0
  72. package/gen-ai/model-manager/index.vue +1583 -0
  73. package/gen-ai/model-manager/mockData.ts +177 -0
  74. package/gen-ai/model-manager/template-detail-shared.ts +154 -0
  75. package/gen-ai/model-manager/template-editor.vue +345 -0
  76. package/gen-ai/model-manager/types/index.ts +40 -0
  77. package/gen-ai/model-manager/utils.ts +25 -0
  78. package/package.json +12 -0
@@ -0,0 +1,2361 @@
1
+ <template>
2
+ <aside
3
+ ref="settingsPanelRef"
4
+ class="chatbot-report-panel chatbot-report-panel--settings"
5
+ :class="{
6
+ 'is-readonly': readonly,
7
+ 'is-compact': !settingsOpen && (readonly || !historyOpen),
8
+ }"
9
+ >
10
+ <ReportCollapsibleSection v-model:open="settingsOpen" title="生成報告設置" card-hover>
11
+ <div class="chatbot-report-settings__content">
12
+ <div class="chatbot-report-chapter-select">
13
+ <div class="chatbot-report-chapter-header">
14
+ <span class="chatbot-report-chapter-header__title">選擇章節及模板</span>
15
+ <span
16
+ class="chatbot-report-chapter-header__col chatbot-report-chapter-header__col--version"
17
+ >版本</span
18
+ >
19
+ <span
20
+ class="chatbot-report-chapter-header__col chatbot-report-chapter-header__col--generator"
21
+ >生成人</span
22
+ >
23
+ </div>
24
+
25
+ <label class="chatbot-report-chapter-select-all">
26
+ <span
27
+ class="chatbot-report-chapter-row__sort chatbot-report-chapter-row__sort--placeholder"
28
+ aria-hidden="true"
29
+ />
30
+ <input
31
+ class="chatbot-report-chapter-row__check"
32
+ type="checkbox"
33
+ :checked="allChaptersSelected"
34
+ :disabled="readonly"
35
+ @change="toggleAllChapters"
36
+ />
37
+ <span class="chatbot-report-chapter-select-all__label">全部章節</span>
38
+ </label>
39
+
40
+ <div class="chatbot-report-chapter-list">
41
+ <div
42
+ v-for="(preset, index) in orderedPresets"
43
+ :key="preset.key"
44
+ class="chatbot-report-chapter-row-wrap"
45
+ :data-chapter-key="preset.key"
46
+ :class="{ 'is-popover-open': openChapterDetailKey === preset.key }"
47
+ >
48
+ <div
49
+ class="chatbot-report-chapter-row"
50
+ :class="{
51
+ 'is-dragging': dragChapterKey === preset.key,
52
+ 'is-drag-over': dragOverChapterKey === preset.key,
53
+ 'is-unavailable': isChapterUnavailable(preset),
54
+ 'is-ellipsis-clickable': readonly || hasChapterRowEllipsis(preset),
55
+ 'is-detail-open': openChapterDetailKey === preset.key,
56
+ }"
57
+ :draggable="!readonly"
58
+ @dragstart="onChapterDragStart($event, preset.key)"
59
+ @dragend="handleChapterDragEnd"
60
+ @dragover.prevent="dragOverChapterKey = preset.key"
61
+ @drop="handleChapterDrop(preset.key)"
62
+ @click="onChapterRowClick(preset.key, $event)"
63
+ >
64
+ <el-icon class="chatbot-report-chapter-row__sort" aria-hidden="true">
65
+ <Sort />
66
+ </el-icon>
67
+ <input
68
+ v-model="selectedChapters[preset.key]"
69
+ class="chatbot-report-chapter-row__check"
70
+ type="checkbox"
71
+ :disabled="readonly || isChapterUnavailable(preset)"
72
+ @click.stop
73
+ @mousedown.stop
74
+ />
75
+ <div class="chatbot-report-chapter-row__label" @mousedown.stop @dragstart.prevent>
76
+ <button
77
+ type="button"
78
+ class="chatbot-report-chapter-row__label-text"
79
+ :class="{ 'is-truncated': labelOverflowMap[preset.key] }"
80
+ draggable="false"
81
+ :ref="(el) => bindChapterLabelEl(preset.key, el)"
82
+ @mousedown.stop
83
+ >
84
+ <span class="chatbot-report-chapter-row__label-main">
85
+ {{ formatChapterDisplayTitle(index, preset.label) }}
86
+ </span>
87
+ <span
88
+ v-if="labelOverflowMap[preset.key]"
89
+ class="chatbot-report-chapter-row__label-ellipsis"
90
+ aria-hidden="true"
91
+ >...</span
92
+ >
93
+ </button>
94
+ </div>
95
+ <template v-if="isChapterUnavailable(preset)">
96
+ <span class="chatbot-report-chapter-row__no-version">{{
97
+ unavailableVersionLabel
98
+ }}</span>
99
+ </template>
100
+ <template v-else>
101
+ <el-select
102
+ v-model="chapterVersions[preset.key]"
103
+ class="chatbot-report-chapter-row__version"
104
+ size="small"
105
+ teleported
106
+ :disabled="readonly || !getChapterVersionOptions(preset.key).length"
107
+ @click.stop
108
+ @mousedown.stop="onVersionSelectMouseDown"
109
+ @visible-change="onChapterVersionVisibleChange"
110
+ >
111
+ <el-option
112
+ v-for="version in getChapterVersionOptions(preset.key)"
113
+ :key="`${preset.key}-${version.value}-${version.text}`"
114
+ :label="version.text"
115
+ :value="version.value"
116
+ />
117
+ </el-select>
118
+ <span
119
+ class="chatbot-report-chapter-row__generator"
120
+ :class="{
121
+ 'is-truncated': isGeneratorNameTruncated(preset.key),
122
+ }"
123
+ >
124
+ <span class="chatbot-report-chapter-row__generator-main">
125
+ {{ formatGeneratorDisplayName(preset.key) }}
126
+ </span>
127
+ <span
128
+ v-if="isGeneratorNameTruncated(preset.key)"
129
+ class="chatbot-report-chapter-row__generator-ellipsis"
130
+ aria-hidden="true"
131
+ >...</span
132
+ >
133
+ </span>
134
+ </template>
135
+ </div>
136
+
137
+ <!-- 浮動詳情:absolute 不佔文檔流空間,覆蓋在後續章節之上 -->
138
+ <div
139
+ v-if="openChapterDetailKey === preset.key && openChapterDetail"
140
+ class="chatbot-report-chapter-title-popover-local"
141
+ role="dialog"
142
+ aria-label="章節詳情"
143
+ @click.stop
144
+ @mousedown.stop
145
+ >
146
+ <div class="chatbot-report-chapter-detail-row">
147
+ <span class="chatbot-report-chapter-detail-row__label">章節標題</span>
148
+ <span class="chatbot-report-chapter-detail-row__value">{{
149
+ openChapterDetail.title
150
+ }}</span>
151
+ </div>
152
+ <div class="chatbot-report-chapter-detail-row">
153
+ <span class="chatbot-report-chapter-detail-row__label">版本</span>
154
+ <span class="chatbot-report-chapter-detail-row__value">{{
155
+ openChapterDetail.version
156
+ }}</span>
157
+ </div>
158
+ <div class="chatbot-report-chapter-detail-row">
159
+ <span class="chatbot-report-chapter-detail-row__label">生成人</span>
160
+ <span class="chatbot-report-chapter-detail-row__value">{{
161
+ openChapterDetail.generator
162
+ }}</span>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+
169
+ <div v-if="!readonly" class="chatbot-report-settings__toolbar">
170
+ <div class="chatbot-report-settings__toolbar-inner">
171
+ <div class="chatbot-report-settings__col-coherence">
172
+ <el-select
173
+ v-if="showChapterLanguageSelect"
174
+ v-model="chapterContentLanguage"
175
+ class="chatbot-report-settings__language"
176
+ size="small"
177
+ teleported
178
+ @mousedown="onVersionSelectMouseDown"
179
+ @visible-change="onChapterVersionVisibleChange"
180
+ >
181
+ <el-option
182
+ v-for="item in chapterContentLanguageOptions"
183
+ :key="item.value"
184
+ :label="item.label"
185
+ :value="item.value"
186
+ />
187
+ </el-select>
188
+ <el-button
189
+ class="chatbot-report-settings__btn chatbot-report-settings__btn--primary chatbot-report-settings__btn--coherence"
190
+ :loading="coherenceChecking"
191
+ :disabled="readonly || coherenceChecking"
192
+ @click="handleCoherenceCheck"
193
+ >
194
+ 報告連貫性檢查
195
+ </el-button>
196
+ </div>
197
+ <el-button
198
+ class="chatbot-report-settings__btn chatbot-report-settings__btn--primary chatbot-report-settings__btn--generate"
199
+ @click="handleGenerate"
200
+ >
201
+ 生成報告
202
+ </el-button>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </ReportCollapsibleSection>
207
+
208
+ <!-- 無報告歷史時默認收起;有內容或滾動定位時可展開 -->
209
+ <ReportCollapsibleSection
210
+ v-if="!readonly"
211
+ v-model:open="historyOpen"
212
+ title="報告歷史"
213
+ card-hover
214
+ >
215
+ <ReportHistoryList
216
+ v-model:active-key="activeHistoryKey"
217
+ :items="reportHistoryItems"
218
+ :show-title="false"
219
+ :editable="false"
220
+ @select="handleSelectHistory"
221
+ />
222
+ </ReportCollapsibleSection>
223
+ </aside>
224
+ </template>
225
+
226
+ <script setup lang="ts">
227
+ import genAiReportEditorApi, {
228
+ pickAdjustInstanceText,
229
+ pickExistsInstanceVersionNo,
230
+ } from '@/api/modules/build-report/reportEditor'
231
+ import { useGenAiBuildReportStore } from '@/store/modules/gen-ai'
232
+ import type {
233
+ GenAiAdjustInstanceSection,
234
+ GenAiInstanceVersionVO,
235
+ GenAiSimTradTargetLang,
236
+ } from '@/types/gen-ai/build-report/reportEditor'
237
+ import { Sort } from '@element-plus/icons-vue'
238
+ import { ElButton, ElIcon, ElMessage, ElMessageBox, ElOption, ElSelect } from 'element-plus'
239
+ import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
240
+ import { useRoute, useRouter } from 'vue-router'
241
+ import { ReportCollapsibleSection, ReportHistoryList } from '../../common'
242
+ import { isGenAiResponseOk } from '../../common/utils/api-res-status'
243
+ import {
244
+ buildChapterOrderForHistoryRecord,
245
+ buildGenerationSignature,
246
+ CHAPTER_CONTENT_LANGUAGE_OPTIONS,
247
+ findFinalizedReportById,
248
+ isCoherenceCheckCompleted,
249
+ loadCoherenceCheckSession,
250
+ markCoherenceCheckCompleted,
251
+ openReportCoherenceCheck,
252
+ saveCoherenceCheckSession,
253
+ serializeChapterLayoutFromState,
254
+ shouldShowChapterLanguageSelect,
255
+ translateChapterLabel,
256
+ translateUnavailableVersionLabel,
257
+ type ChapterContentLanguage,
258
+ type CoherenceCheckSessionPayload,
259
+ type ReportChapterSelectionItem,
260
+ } from '../report-domain'
261
+ import { useReportWorkflow, type ReportHistoryItem } from '../useReportWorkflow'
262
+
263
+ const props = withDefaults(
264
+ defineProps<{
265
+ readonly?: boolean
266
+ coherenceSession?: CoherenceCheckSessionPayload | null
267
+ }>(),
268
+ {
269
+ readonly: false,
270
+ coherenceSession: null,
271
+ }
272
+ )
273
+
274
+ const UNAVAILABLE_CHAPTER_LABELS = new Set(['附件與數'])
275
+
276
+ interface ChapterPreset {
277
+ key: string
278
+ label: string
279
+ }
280
+
281
+ const emit = defineEmits<{
282
+ generated: [{ historyKey: string; displayTitle: string }]
283
+ }>()
284
+
285
+ const settingsOpen = ref(true)
286
+ const historyOpen = ref(false)
287
+ const route = useRoute()
288
+ const router = useRouter()
289
+ const {
290
+ selectedTemplate,
291
+ draft,
292
+ patchDraft,
293
+ historyPreviewRecordId,
294
+ historyPreviewLayoutBaseline,
295
+ historyPreviewApplyToken,
296
+ markHistoryPreviewDirty,
297
+ selectHistoryReport,
298
+ reportHistoryItems,
299
+ activeHistoryKey,
300
+ historyScrollTargetKey,
301
+ clearHistoryScrollTarget,
302
+ resetHistoryPreviewState,
303
+ settingsChapterPresets,
304
+ settingsSelectedChapters,
305
+ settingsChapterOrder,
306
+ settingsChapterVersionOptions,
307
+ settingsChapterVersions,
308
+ settingsChapterGenerators,
309
+ applyAdjustInstanceFiles,
310
+ requestSettingsTypewriterReplay,
311
+ settingsTypingChapterKey,
312
+ adjustInstanceContents,
313
+ setAdjustInstanceContents,
314
+ existsInstanceVersionNo,
315
+ setExistsInstanceVersionNo,
316
+ applyAdjustInstanceSections,
317
+ setCoherenceCheckResultReady,
318
+ clearCoherenceCheckResultReady,
319
+ coherenceCheckContentText,
320
+ coherenceResultReady,
321
+ markAdjustInstanceSettingSynced,
322
+ isAdjustInstanceSettingSynced,
323
+ suppressAdjustInstanceSettingWatch,
324
+ beginReportGenerateStream,
325
+ appendReportGenerateStream,
326
+ finishReportGenerateStream,
327
+ clearReportGenerateStream,
328
+ sourceFiles,
329
+ upsertChapterChartStreamRef,
330
+ clearChapterChartStreamRefs,
331
+ } = useReportWorkflow()
332
+
333
+ const genAiBuildReportStore = useGenAiBuildReportStore()
334
+
335
+ /** 本地測試:優先 store.instanceId */
336
+ function resolveInstanceId() {
337
+ return String(genAiBuildReportStore.instanceId ?? '').trim()
338
+ }
339
+
340
+ // report editor api
341
+ /** 報告歷史接口緩存(InstHistEnquiry.instanceVersionVOList) */
342
+ const genAiInstanceVersionVOList = ref<GenAiInstanceVersionVO[]>([])
343
+ /** 避免佈局回填 / 初始化時誤觸 AdjustInstanceSetting */
344
+ let suppressAdjustInstanceSetting = false
345
+ /** 用戶手動點報告歷史時,暫不因 existsInstanceVersionNo 回寫改選中 */
346
+ let suppressHistorySelectByExistsVersion = false
347
+ let adjustInstanceSettingSeq = 0
348
+ let simTradTransferSeq = 0
349
+ /** 中欄正文指紋:相同內容不重播打字機 */
350
+ let lastAdjustContentsFingerprint = ''
351
+
352
+ function fingerprintAdjustContents(
353
+ list: Array<{ sectionTitle?: string; sectionContent?: string }>
354
+ ) {
355
+ return JSON.stringify(
356
+ list.map((item) => ({
357
+ t: String(item.sectionTitle ?? ''),
358
+ c: String(item.sectionContent ?? ''),
359
+ }))
360
+ )
361
+ }
362
+
363
+ /**
364
+ * 按章節標題還原用戶拖拽後的順序。
365
+ * 背景:AdjustInstanceSetting 回寫調用 applyAdjustInstanceSections 時,
366
+ * 會把 settingsChapterOrder 重置為 chapter-0..n(接口列表下標),
367
+ * 導致「取消全選 → 拖拽 → 再勾全部章節」後打字機按接口序打印而非拖拽序。
368
+ */
369
+ function restoreChapterOrderByLabels(orderLabels: string[]) {
370
+ if (!orderLabels.length || !settingsChapterPresets.value.length) return
371
+ // 標題 → 當前 key(回寫後 key 可能重編,只能用標題對齊)
372
+ const keyByLabel = new Map(
373
+ settingsChapterPresets.value.map((preset) => [preset.label, preset.key] as const)
374
+ )
375
+ const restored = orderLabels
376
+ .map((label) => keyByLabel.get(label))
377
+ .filter((key): key is string => typeof key === 'string')
378
+ if (!restored.length) return
379
+ const used = new Set(restored)
380
+ // 拖拽序裡沒有的章節(若有)接在後面,避免丟章
381
+ const rest = settingsChapterPresets.value
382
+ .map((preset) => preset.key)
383
+ .filter((key) => !used.has(key))
384
+ settingsChapterOrder.value = [...restored, ...rest]
385
+ }
386
+
387
+ function splitCommaIds(raw: string | undefined | null): string[] {
388
+ if (!raw) return []
389
+ return String(raw)
390
+ .split(',')
391
+ .map((item) => item.trim())
392
+ .filter(Boolean)
393
+ }
394
+
395
+ /** 統一版本號:V1 / v1 / 1 → V1,便於 existsInstanceVersionNo 對照 */
396
+ function normalizeInstanceVersionNo(raw: unknown): string {
397
+ if (raw == null || raw === '') return ''
398
+ const text = String(raw).trim().toUpperCase()
399
+ if (!text) return ''
400
+ return text.startsWith('V') ? text : `V${text}`
401
+ }
402
+
403
+ function mapInstanceVersionToHistoryItem(
404
+ item: GenAiInstanceVersionVO,
405
+ index: number
406
+ ): ReportHistoryItem {
407
+ const versionNo = item.instanceVersionNo
408
+ const titleFromNo =
409
+ versionNo != null && String(versionNo).trim() ? `報告版本 V${String(versionNo).trim()}` : ''
410
+ const title = item.objectName?.trim() || titleFromNo || `報告歷史 ${index + 1}`
411
+ const key =
412
+ item.instanceVersionId?.trim() ||
413
+ `${item.instanceId || 'hist'}-${index}-${item.sectionVersionIdList || ''}`
414
+
415
+ return {
416
+ key,
417
+ title,
418
+ author: item.updateUserName || item.createdUserName || '',
419
+ finalizedAt: item.updateDatetime || item.createdDatetime || '',
420
+ recordId: key,
421
+ kind: 'history',
422
+ }
423
+ }
424
+
425
+ /** 當前 versionIdList(上→下)是否與某條報告歷史 sectionVersionIdList 一致 */
426
+ function findHistoryMatchingVersionIds(
427
+ versionIdList: string[]
428
+ ): GenAiInstanceVersionVO | undefined {
429
+ if (!versionIdList.length) return undefined
430
+ const target = versionIdList.join(',')
431
+ return genAiInstanceVersionVOList.value.find((item) => {
432
+ const ids = splitCommaIds(item.sectionVersionIdList)
433
+ return ids.length === versionIdList.length && ids.join(',') === target
434
+ })
435
+ }
436
+
437
+ /** 已生成報告展示標題(接口歷史 / existsInstanceVersionNo) */
438
+ function resolveExistingReportDisplayTitle(vo?: GenAiInstanceVersionVO | null): string {
439
+ if (vo) {
440
+ const versionNo = vo.instanceVersionNo
441
+ const titleFromNo =
442
+ versionNo != null && String(versionNo).trim() ? `報告版本 ${String(versionNo).trim()}` : ''
443
+ return (
444
+ vo.objectName?.trim() ||
445
+ titleFromNo ||
446
+ `${draft.reportName || '報告'}${existsInstanceVersionNo.value ? ` ${existsInstanceVersionNo.value}` : ''}`
447
+ )
448
+ }
449
+ if (existsInstanceVersionNo.value) {
450
+ const name = draft.reportName?.trim() || '報告'
451
+ return `${name} ${existsInstanceVersionNo.value}`
452
+ }
453
+ return draft.reportName?.trim() || '報告'
454
+ }
455
+
456
+ /**
457
+ * 「已生成報告」校驗:僅對照 InstHistEnquiry 章節版本匹配
458
+ */
459
+ async function findAlreadyGeneratedReportTitle(
460
+ _templateId: string,
461
+ _selection: ReportChapterSelectionItem[]
462
+ ): Promise<string | null> {
463
+ const versionIdList = buildSelectedVersionIdList()
464
+ const instanceId = resolveInstanceId()
465
+ if (instanceId) {
466
+ await loadGenAiInstanceHistForReportPage()
467
+ }
468
+
469
+ const matchedHist = findHistoryMatchingVersionIds(versionIdList)
470
+ if (matchedHist) {
471
+ return resolveExistingReportDisplayTitle(matchedHist)
472
+ }
473
+ return null
474
+ }
475
+
476
+ /**
477
+ * 報告歷史查詢 InstHistEnquiry
478
+ * 調試:參數僅 instanceId;返回 instanceVersionVOList
479
+ */
480
+ const fetchGenAiInstanceHist = async (params: { instanceId: string }) => {
481
+ const res: any = await genAiReportEditorApi
482
+ .fetchGenAiInstanceHist({
483
+ instanceId: params.instanceId,
484
+ })
485
+ .catch((err) => {
486
+ console.error('接口异常', err)
487
+ return null
488
+ })
489
+
490
+ try {
491
+ if (!isGenAiResponseOk(res)) {
492
+ console.warn('[report editor] InstHistEnquiry', res?.message || '接口异常')
493
+ return {
494
+ ok: false as const,
495
+ list: [] as GenAiInstanceVersionVO[],
496
+ }
497
+ }
498
+ const list = (
499
+ Array.isArray(res?.data?.instanceVersionVOList) ? res.data.instanceVersionVOList : []
500
+ ) as GenAiInstanceVersionVO[]
501
+ console.log('报告历史列表', list)
502
+ return { ok: true as const, list }
503
+ } catch (err) {
504
+ console.error('接口异常', err)
505
+ return {
506
+ ok: false as const,
507
+ list: [] as GenAiInstanceVersionVO[],
508
+ }
509
+ }
510
+ }
511
+
512
+ /** 拉取報告歷史並覆蓋右側列表;失敗則空並收起 */
513
+ async function loadGenAiInstanceHistForReportPage() {
514
+ const instanceId = resolveInstanceId()
515
+ if (!instanceId) {
516
+ console.warn('[report editor] 無 instanceId,InstHistEnquiry 跳過')
517
+ genAiInstanceVersionVOList.value = []
518
+ reportHistoryItems.value = []
519
+ activeHistoryKey.value = ''
520
+ historyOpen.value = false
521
+ return false
522
+ }
523
+
524
+ const result = await fetchGenAiInstanceHist({ instanceId })
525
+ if (!result.ok) {
526
+ genAiInstanceVersionVOList.value = []
527
+ reportHistoryItems.value = []
528
+ activeHistoryKey.value = ''
529
+ historyOpen.value = false
530
+ return false
531
+ }
532
+
533
+ genAiInstanceVersionVOList.value = result.list
534
+ reportHistoryItems.value = result.list.map((item, index) =>
535
+ mapInstanceVersionToHistoryItem(item, index)
536
+ )
537
+ historyOpen.value = reportHistoryItems.value.length > 0
538
+ // 進頁:existsInstanceVersionNo === instanceVersionNo → 選中第一個匹配 card
539
+ selectHistoryMatchingExistsVersionNo()
540
+ return true
541
+ }
542
+
543
+ /**
544
+ * AdjustInstance.existsInstanceVersionNo 對照報告歷史 instanceVersionNo
545
+ * 相等則該 card 為選中態(多個匹配取第一個);無匹配則選列表首條
546
+ */
547
+ function selectHistoryMatchingExistsVersionNo() {
548
+ if (!reportHistoryItems.value.length) {
549
+ activeHistoryKey.value = ''
550
+ return
551
+ }
552
+ const target = normalizeInstanceVersionNo(existsInstanceVersionNo.value)
553
+ if (target) {
554
+ const matchIndex = genAiInstanceVersionVOList.value.findIndex(
555
+ (item) => normalizeInstanceVersionNo(item.instanceVersionNo) === target
556
+ )
557
+ if (matchIndex >= 0) {
558
+ activeHistoryKey.value = reportHistoryItems.value[matchIndex]?.key ?? ''
559
+ console.log('[report editor] 報告歷史選中(對照 existsInstanceVersionNo)', {
560
+ existsInstanceVersionNo: existsInstanceVersionNo.value,
561
+ key: activeHistoryKey.value,
562
+ })
563
+ return
564
+ }
565
+ }
566
+ activeHistoryKey.value = reportHistoryItems.value[0]?.key ?? ''
567
+ }
568
+
569
+ /**
570
+ * 已勾選章節的版本 UUID 列表(上→下)
571
+ * = instanceSections.versions[].value
572
+ */
573
+ function buildSelectedVersionIdList(): string[] {
574
+ return chapterOrder.value
575
+ .map((key) => chapterPresets.value.find((preset) => preset.key === key))
576
+ .filter((preset): preset is ChapterPreset => !!preset)
577
+ .filter((preset) => !!selectedChapters.value[preset.key] && !isChapterUnavailable(preset))
578
+ .map((preset) => String(chapterVersions.value[preset.key] ?? '').trim())
579
+ .filter(Boolean)
580
+ }
581
+
582
+ /**
583
+ * 調整報告設置 AdjustInstanceSetting
584
+ * 勾選章節 / 拖動 / 切換報告歷史後調用
585
+ * 回寫:資訊源 +(若有)右側章節設置 + 中欄正文
586
+ */
587
+ const adjustGenAiInstanceSetting = async (params: {
588
+ instanceId: string
589
+ versionIdList: string[]
590
+ }) => {
591
+ const res: any = await genAiReportEditorApi
592
+ .adjustGenAiInstanceSetting({
593
+ instanceId: params.instanceId,
594
+ versionIdList: params.versionIdList,
595
+ })
596
+ .catch((err) => {
597
+ console.error('接口异常', err)
598
+ return null
599
+ })
600
+
601
+ try {
602
+ if (!isGenAiResponseOk(res)) {
603
+ console.warn('[report editor] AdjustInstanceSetting', res?.message || '接口异常')
604
+ return { ok: false as const }
605
+ }
606
+ const data = res?.data ?? {}
607
+ const fileList = Array.isArray(data.instanceFileList) ? data.instanceFileList : []
608
+ const sectionList = Array.isArray(data.instanceSections) ? data.instanceSections : []
609
+ const contentList = Array.isArray(data.instanceContents) ? data.instanceContents : []
610
+
611
+ // —— 保留拖拽順序:回寫前先記下當前標題序,回寫後還原 ——
612
+ const preservedOrderLabels = settingsChapterOrder.value
613
+ .map((key) => settingsChapterPresets.value.find((preset) => preset.key === key)?.label)
614
+ .filter((label): label is string => !!label)
615
+
616
+ // 回寫會改 chapterOrder / versions → 抑制 watch,避免死循環閃爍
617
+ suppressAdjustInstanceSetting = true
618
+ try {
619
+ setExistsInstanceVersionNo(pickExistsInstanceVersionNo(data))
620
+ applyAdjustInstanceFiles(fileList)
621
+
622
+ // 右側「生成報告設置」章節勾選 / 版本 / 排序
623
+ let orderRestoredToDrag = false
624
+ if (sectionList.length) {
625
+ applyAdjustInstanceSections(sectionList)
626
+ // 接口剛寫入的默認序(chapter-0..n)
627
+ const apiOrderKeys = settingsChapterOrder.value.slice()
628
+ // 還原為用戶拖拽後的標題序
629
+ restoreChapterOrderByLabels(preservedOrderLabels)
630
+ // 是否相對接口默認序發生了拖拽重排(用於決定是否強制重播打字機)
631
+ orderRestoredToDrag =
632
+ JSON.stringify(settingsChapterOrder.value) !== JSON.stringify(apiOrderKeys)
633
+ }
634
+ // 中欄 editor 正文:
635
+ // 1) 內容指紋變了 → 按還原後的首章重播打字機
636
+ // 2) 內容沒變但拖拽序變了 → 仍按新順序重播,避免打印序錯亂
637
+ if (contentList.length) {
638
+ const fp = fingerprintAdjustContents(contentList)
639
+ // 0924:原样传入,保留 instanceContents[].refList
640
+ setAdjustInstanceContents(contentList)
641
+ // 0924
642
+ const firstKey = settingsChapterOrder.value[0]
643
+ if (fp !== lastAdjustContentsFingerprint) {
644
+ lastAdjustContentsFingerprint = fp
645
+ if (firstKey) requestSettingsTypewriterReplay(firstKey)
646
+ } else if (orderRestoredToDrag && firstKey) {
647
+ requestSettingsTypewriterReplay(firstKey)
648
+ }
649
+ }
650
+
651
+ markAdjustInstanceSettingSynced(buildSelectedVersionIdList())
652
+ } finally {
653
+ void nextTick(() => {
654
+ suppressAdjustInstanceSetting = false
655
+ })
656
+ }
657
+
658
+ console.log('调整报告设置', {
659
+ versionIdList: params.versionIdList,
660
+ existsInstanceVersionNo: existsInstanceVersionNo.value,
661
+ sections: sectionList.length,
662
+ contents: contentList.length,
663
+ fileList,
664
+ })
665
+ return { ok: true as const }
666
+ } catch (err) {
667
+ console.error('接口异常', err)
668
+ return { ok: false as const }
669
+ }
670
+ }
671
+
672
+ /** 勾選 / 拖動 / 切換章節版本後調用;全部不選不調用(原有佈局聯動 + 資訊源反顯) */
673
+ async function loadAdjustInstanceSettingOnChapterChange() {
674
+ if (props.readonly) return false
675
+ if (
676
+ suppressAdjustInstanceSetting ||
677
+ suppressAdjustInstanceSettingWatch.value ||
678
+ isApplyingHistoryLayout.value
679
+ ) {
680
+ return false
681
+ }
682
+ // 尚無接口版本選項時不調(避免模板 mock V1..V6)
683
+ if (!Object.keys(settingsChapterVersionOptions.value).length) return false
684
+
685
+ const versionIdList = buildSelectedVersionIdList()
686
+ if (!versionIdList.length) {
687
+ console.log('[report editor] 未勾選章節,跳過 AdjustInstanceSetting')
688
+ return false
689
+ }
690
+ // 與上次回寫結果一致 → 跳過(打斷回寫觸發的 watch 循環)
691
+ if (isAdjustInstanceSettingSynced(versionIdList)) return false
692
+
693
+ const instanceId = resolveInstanceId()
694
+ if (!instanceId) {
695
+ console.warn('[report editor] 無 instanceId,AdjustInstanceSetting 跳過')
696
+ return false
697
+ }
698
+
699
+ const seq = ++adjustInstanceSettingSeq
700
+ const result = await adjustGenAiInstanceSetting({ instanceId, versionIdList })
701
+ if (seq !== adjustInstanceSettingSeq) return false
702
+ return result.ok
703
+ }
704
+
705
+ /**
706
+ * 報告定稿 FinalInstance
707
+ * 生成報告設置頁點「生成報告」後調用;參數同 AdjustInstanceSetting
708
+ */
709
+ const finalGenAiInstance = async (params: { instanceId: string; versionIdList: string[] }) => {
710
+ const res: any = await genAiReportEditorApi
711
+ .finalGenAiInstance({
712
+ instanceId: params.instanceId,
713
+ versionIdList: params.versionIdList,
714
+ })
715
+ .catch((err) => {
716
+ console.error('接口异常', err)
717
+ return null
718
+ })
719
+
720
+ try {
721
+ if (!isGenAiResponseOk(res)) {
722
+ console.warn('[report editor] FinalInstance', res?.message || '接口异常')
723
+ return { ok: false as const, message: String(res?.message || '報告定稿失敗') }
724
+ }
725
+ console.log('报告定稿 FinalInstance', {
726
+ versionIdList: params.versionIdList,
727
+ data: res?.data,
728
+ })
729
+ return { ok: true as const, message: String(res?.message || '') }
730
+ } catch (err) {
731
+ console.error('接口异常', err)
732
+ return { ok: false as const, message: '報告定稿失敗' }
733
+ }
734
+ }
735
+
736
+ /** 點「生成報告」→ FinalInstance;失敗不走本地定稿 */
737
+ async function loadFinalInstanceOnGenerate() {
738
+ const versionIdList = buildSelectedVersionIdList()
739
+ if (!versionIdList.length) {
740
+ ElMessage.warning('請先勾選章節')
741
+ return false
742
+ }
743
+
744
+ const instanceId = resolveInstanceId()
745
+ if (!instanceId) {
746
+ console.warn('[report editor] 無 instanceId,FinalInstance 跳過')
747
+ ElMessage.warning('缺少報告實例,無法生成')
748
+ return false
749
+ }
750
+
751
+ const result = await finalGenAiInstance({ instanceId, versionIdList })
752
+ if (!result.ok) {
753
+ ElMessage.warning(result.message || '報告定稿失敗')
754
+ return false
755
+ }
756
+ // 定稿成功後刷新報告歷史
757
+ await loadGenAiInstanceHistForReportPage()
758
+ return true
759
+ }
760
+
761
+ /** zh-Hans → CHS(簡體),zh-Hant → CHT(繁體) */
762
+ function mapChapterContentLanguageToTargetLang(
763
+ lang: ChapterContentLanguage
764
+ ): GenAiSimTradTargetLang {
765
+ return lang === 'zh-Hans' ? 'CHS' : 'CHT'
766
+ }
767
+
768
+ /**
769
+ * 繁簡轉換 SimTradTransfer
770
+ * 生成報告設置「語言」下拉切換後調用;回寫中欄 instanceContents
771
+ */
772
+ const simTradTransferGenAiInstance = async (params: {
773
+ targetLang: GenAiSimTradTargetLang
774
+ instanceContents: Array<{ sectionTitle: string; sectionContent: string }>
775
+ }) => {
776
+ const res: any = await genAiReportEditorApi
777
+ .simTradTransferGenAiInstance({
778
+ targetLang: params.targetLang,
779
+ instanceContents: params.instanceContents,
780
+ })
781
+ .catch((err) => {
782
+ console.error('接口异常', err)
783
+ return null
784
+ })
785
+
786
+ try {
787
+ if (!isGenAiResponseOk(res)) {
788
+ console.warn('[report editor] SimTradTransfer', res?.message || '接口异常')
789
+ return { ok: false as const }
790
+ }
791
+ const raw = res?.data
792
+ const list = Array.isArray(raw?.instanceContents)
793
+ ? raw.instanceContents
794
+ : Array.isArray(raw)
795
+ ? raw
796
+ : []
797
+ // 0924:原样传入,保留 instanceContents[].refList
798
+ setAdjustInstanceContents(list)
799
+ // 0924
800
+ lastAdjustContentsFingerprint = fingerprintAdjustContents(adjustInstanceContents.value)
801
+ console.log('繁简转换 SimTradTransfer', {
802
+ targetLang: params.targetLang,
803
+ contents: adjustInstanceContents.value,
804
+ })
805
+ return { ok: true as const }
806
+ } catch (err) {
807
+ console.error('接口异常', err)
808
+ return { ok: false as const }
809
+ }
810
+ }
811
+
812
+ /** 語言下拉:先走原有 draft 語言切換,再調繁簡轉換;無正文時僅保留本地語言(標題 i18n) */
813
+ async function loadSimTradTransferOnLanguageChange(lang: ChapterContentLanguage) {
814
+ if (props.readonly) return false
815
+ const contents = adjustInstanceContents.value
816
+ if (!contents.length) {
817
+ console.log('[report editor] 無 instanceContents,跳過 SimTradTransfer(保留本地語言切換)')
818
+ return false
819
+ }
820
+
821
+ const targetLang = mapChapterContentLanguageToTargetLang(lang)
822
+ const seq = ++simTradTransferSeq
823
+ const result = await simTradTransferGenAiInstance({
824
+ targetLang,
825
+ instanceContents: contents.map((item) => ({
826
+ sectionTitle: item.sectionTitle,
827
+ sectionContent: item.sectionContent,
828
+ })),
829
+ })
830
+ if (seq !== simTradTransferSeq) return false
831
+ if (!result.ok) {
832
+ ElMessage.warning('繁簡轉換失敗')
833
+ return false
834
+ }
835
+ // 原有中欄重播:內容已換
836
+ const firstKey = settingsChapterOrder.value[0]
837
+ if (firstKey) requestSettingsTypewriterReplay(firstKey)
838
+ return true
839
+ }
840
+
841
+ // —— report editor api end ——
842
+
843
+ function handleSelectHistory(item: ReportHistoryItem) {
844
+ activeHistoryKey.value = item.key
845
+ const voIndex = reportHistoryItems.value.findIndex((entry) => entry.key === item.key)
846
+ const vo = voIndex >= 0 ? genAiInstanceVersionVOList.value[voIndex] : undefined
847
+ if (vo) {
848
+ // 切換報告歷史 → AdjustInstanceSetting 刷新:生成報告設置 + 中欄 editor + 資訊源
849
+ const versionIdList = splitCommaIds(vo.sectionVersionIdList)
850
+ const instanceId = String(vo.instanceId ?? '').trim() || resolveInstanceId()
851
+ console.log('選中報告歷史', {
852
+ instanceId,
853
+ sectionIds: splitCommaIds(vo.sectionIdList),
854
+ sectionVersionIds: versionIdList,
855
+ instanceVersionNo: vo.instanceVersionNo,
856
+ vo,
857
+ })
858
+ if (instanceId && versionIdList.length) {
859
+ void loadAdjustInstanceSettingFromHistoryCard({ instanceId, versionIdList })
860
+ } else {
861
+ ElMessage.warning('該歷史缺少版本信息,無法刷新報告設置')
862
+ }
863
+ resetHistoryPreviewState()
864
+ return
865
+ }
866
+ if (!item.recordId) {
867
+ resetHistoryPreviewState()
868
+ return
869
+ }
870
+ // 本地定稿歷史:原有預覽邏輯
871
+ selectHistoryReport(item.recordId)
872
+ }
873
+
874
+ /**
875
+ * 點擊報告歷史 card → AdjustInstanceSetting 刷新頁面(資訊源等)
876
+ * 參數取自該 card 的 instanceId + sectionVersionIdList
877
+ */
878
+ async function loadAdjustInstanceSettingFromHistoryCard(params: {
879
+ instanceId: string
880
+ versionIdList: string[]
881
+ }) {
882
+ if (props.readonly) return false
883
+ const selectedKey = activeHistoryKey.value
884
+ suppressAdjustInstanceSetting = true
885
+ suppressHistorySelectByExistsVersion = true
886
+ const seq = ++adjustInstanceSettingSeq
887
+ const result = await adjustGenAiInstanceSetting(params)
888
+ if (seq !== adjustInstanceSettingSeq) return false
889
+ // 保持用戶點選的 card
890
+ if (selectedKey) activeHistoryKey.value = selectedKey
891
+ void nextTick(() => {
892
+ suppressAdjustInstanceSetting = false
893
+ suppressHistorySelectByExistsVersion = false
894
+ })
895
+ return result.ok
896
+ }
897
+
898
+ /** 中欄打字進度經 workflow 通知;右側自行滾動列表行,不依賴中欄 DOM */
899
+ watch(settingsTypingChapterKey, (key) => {
900
+ if (!key) return
901
+ void nextTick(() => {
902
+ const row = document.querySelector(
903
+ `.chatbot-report-chapter-row-wrap[data-chapter-key="${CSS.escape(key)}"]`
904
+ ) as HTMLElement | null
905
+ row?.scrollIntoView({ block: 'nearest', behavior: 'smooth', inline: 'nearest' })
906
+ })
907
+ })
908
+
909
+ watch(historyScrollTargetKey, async (key) => {
910
+ if (!key) return
911
+ historyOpen.value = true
912
+ await nextTick()
913
+ clearHistoryScrollTarget()
914
+ })
915
+
916
+ /** 無報告歷史時收起面板,避免空內容佔位 */
917
+ watch(
918
+ reportHistoryItems,
919
+ (items) => {
920
+ if (!items.length) historyOpen.value = false
921
+ },
922
+ { deep: true }
923
+ )
924
+
925
+ const chapterContentLanguageOptions = CHAPTER_CONTENT_LANGUAGE_OPTIONS
926
+
927
+ const showChapterLanguageSelect = computed(() => shouldShowChapterLanguageSelect(draft.language))
928
+
929
+ const chapterContentLanguage = computed({
930
+ get: () => draft.chapterContentLanguage,
931
+ set: (value: ChapterContentLanguage) => {
932
+ if (draft.chapterContentLanguage === value) return
933
+ patchDraft({ chapterContentLanguage: value })
934
+ // 語言下拉:CHS/CHT 繁簡轉換
935
+ void loadSimTradTransferOnLanguageChange(value)
936
+ },
937
+ })
938
+
939
+ const unavailableVersionLabel = computed(() =>
940
+ translateUnavailableVersionLabel(draft.language, 'zh-Hant')
941
+ )
942
+
943
+ const chapterPresets = settingsChapterPresets
944
+ const selectedChapters = settingsSelectedChapters
945
+ /** 與 AdjustInstance 共用:value=UUID(或回落 V1..V6) */
946
+ const chapterVersions = settingsChapterVersions
947
+ const chapterGenerators = settingsChapterGenerators
948
+ const chapterOrder = settingsChapterOrder
949
+ const dragChapterKey = ref<string | null>(null)
950
+ const dragOverChapterKey = ref<string | null>(null)
951
+ const isApplyingHistoryLayout = ref(false)
952
+ let chapterRowDragMoved = false
953
+
954
+ /** 章節版本下拉:僅接口 versions;無則空(不回落 V1..V6 mock) */
955
+ function getChapterVersionOptions(key: string) {
956
+ if (props.readonly) {
957
+ const text = String(chapterVersions.value[key] ?? '').trim()
958
+ const uploadUsrNameDisplay = String(chapterGenerators.value[key] ?? '').trim()
959
+ return text
960
+ ? [{ text, value: text, selected: true, uploadUsrNameDisplay }]
961
+ : []
962
+ }
963
+ return settingsChapterVersionOptions.value[key] ?? []
964
+ }
965
+
966
+ /** 詳情彈層展示 text,而非 UUID */
967
+ function getChapterVersionDisplayText(key: string) {
968
+ const value = chapterVersions.value[key]
969
+ const opt = getChapterVersionOptions(key).find((item) => item.value === value)
970
+ return opt?.text || value || '—'
971
+ }
972
+
973
+ /**
974
+ * 生成人展示:≤3 字完整顯示;>3 字只顯示前 4 字 + ...(對齊圖中 LOCA...)
975
+ * 完整資訊在詳情彈層
976
+ */
977
+ const GENERATOR_NAME_FULL_MAX = 3
978
+ const GENERATOR_NAME_TRUNCATED_CHARS = 4
979
+
980
+ function getGeneratorNameChars(key: string) {
981
+ const name = getChapterVersionOptions(key).find(
982
+ (item) => item.value == chapterVersions.value[key]
983
+ )?.uploadUsrNameDisplay
984
+ return String(name ?? '').trim()
985
+ }
986
+
987
+ function isGeneratorNameTruncated(key: string) {
988
+ return Array.from(getGeneratorNameChars(key)).length > GENERATOR_NAME_FULL_MAX
989
+ }
990
+
991
+ function formatGeneratorDisplayName(key: string) {
992
+ const chars = Array.from(getGeneratorNameChars(key))
993
+ if (!chars.length) return ''
994
+ if (chars.length <= GENERATOR_NAME_FULL_MAX) return chars.join('')
995
+ return chars.slice(0, GENERATOR_NAME_TRUNCATED_CHARS).join('')
996
+ }
997
+
998
+ const orderedPresets = computed(() =>
999
+ chapterOrder.value
1000
+ .map((key) => chapterPresets.value.find((preset) => preset.key === key))
1001
+ .filter((preset): preset is ChapterPreset => !!preset)
1002
+ )
1003
+
1004
+ const allChaptersSelected = computed(() => {
1005
+ const selectable = chapterPresets.value.filter((preset) => !isChapterUnavailable(preset))
1006
+ return selectable.length > 0 && selectable.every((preset) => selectedChapters.value[preset.key])
1007
+ })
1008
+
1009
+ function isChapterUnavailable(preset: ChapterPreset) {
1010
+ return UNAVAILABLE_CHAPTER_LABELS.has(preset.label)
1011
+ }
1012
+
1013
+ function formatChapterDisplayTitle(_index: number, label: string) {
1014
+ return translateChapterLabel(label, draft.language, 'zh-Hant')
1015
+ }
1016
+
1017
+ /** 章節標題是否溢出;完整詳情掛在當前章節行下方 */
1018
+ const labelOverflowMap = reactive<Record<string, boolean>>({})
1019
+ const labelElMap = new Map<string, HTMLElement>()
1020
+ const openChapterDetailKey = ref<string | null>(null)
1021
+ const openChapterDetail = ref<{ title: string; version: string; generator: string } | null>(null)
1022
+ const settingsPanelRef = ref<HTMLElement | null>(null)
1023
+ let labelResizeObserver: ResizeObserver | null = null
1024
+
1025
+ function measureChapterLabelOverflow(key: string) {
1026
+ const el = labelElMap.get(key)
1027
+ if (!el) {
1028
+ labelOverflowMap[key] = false
1029
+ return
1030
+ }
1031
+ const main = el.querySelector('.chatbot-report-chapter-row__label-main') as HTMLElement | null
1032
+ if (!main) {
1033
+ labelOverflowMap[key] = false
1034
+ return
1035
+ }
1036
+ labelOverflowMap[key] = main.scrollWidth > main.clientWidth + 1
1037
+ }
1038
+
1039
+ function bindChapterLabelEl(key: string, el: unknown) {
1040
+ const prev = labelElMap.get(key)
1041
+ if (prev && labelResizeObserver) labelResizeObserver.unobserve(prev)
1042
+
1043
+ const node = el as HTMLElement | null
1044
+ if (!node) {
1045
+ labelElMap.delete(key)
1046
+ labelOverflowMap[key] = false
1047
+ return
1048
+ }
1049
+
1050
+ labelElMap.set(key, node)
1051
+ labelResizeObserver?.observe(node)
1052
+ const shell = node.closest('.chatbot-report-chapter-row__label')
1053
+ if (shell) labelResizeObserver?.observe(shell)
1054
+ void nextTick(() => measureChapterLabelOverflow(key))
1055
+ }
1056
+
1057
+ function remeasureAllChapterLabels() {
1058
+ for (const key of labelElMap.keys()) measureChapterLabelOverflow(key)
1059
+ }
1060
+
1061
+ function buildChapterDetail(key: string) {
1062
+ const presetIndex = orderedPresets.value.findIndex((item) => item.key === key)
1063
+ const preset = orderedPresets.value[presetIndex]
1064
+ if (!preset) return null
1065
+ return {
1066
+ title: formatChapterDisplayTitle(presetIndex, preset.label),
1067
+ version: isChapterUnavailable(preset)
1068
+ ? unavailableVersionLabel.value
1069
+ : getChapterVersionDisplayText(preset.key),
1070
+ generator: getGeneratorNameChars(key) || '',
1071
+ }
1072
+ }
1073
+
1074
+ function closeChapterDetailPopover() {
1075
+ openChapterDetailKey.value = null
1076
+ openChapterDetail.value = null
1077
+ }
1078
+
1079
+ /** 關閉面板內已展開的 el-select 下拉(與詳情彈層互斥) */
1080
+ function closeOpenSelectDropdowns() {
1081
+ const panel = settingsPanelRef.value
1082
+ if (!panel) return
1083
+ panel
1084
+ .querySelectorAll<HTMLElement>('.el-select__wrapper[aria-expanded="true"]')
1085
+ .forEach((node) => {
1086
+ node.blur()
1087
+ node.click()
1088
+ })
1089
+ // teleported 下拉:Escape 再兜底關閉
1090
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
1091
+ }
1092
+
1093
+ /** 點選版本 / 語言下拉:立刻收起詳情 */
1094
+ function onVersionSelectMouseDown() {
1095
+ closeChapterDetailPopover()
1096
+ }
1097
+
1098
+ /** 打開版本 / 語言下拉時關閉章節詳情 */
1099
+ function onChapterVersionVisibleChange(open: boolean) {
1100
+ if (open) closeChapterDetailPopover()
1101
+ }
1102
+
1103
+ function hasChapterRowEllipsis(preset: ChapterPreset) {
1104
+ const titleOverflow = !!labelOverflowMap[preset.key]
1105
+ const generatorOverflow =
1106
+ !isChapterUnavailable(preset) && isGeneratorNameTruncated(preset.key)
1107
+ return titleOverflow || generatorOverflow
1108
+ }
1109
+
1110
+ function toggleChapterDetailPopover(key: string) {
1111
+ const preset = orderedPresets.value.find((item) => item.key === key)
1112
+ if (!preset) {
1113
+ closeChapterDetailPopover()
1114
+ return
1115
+ }
1116
+ // 可編輯:僅標題/生成人截斷時可開詳情;只讀檢查頁:任意行可查看完整資訊
1117
+ if (!props.readonly && !hasChapterRowEllipsis(preset)) {
1118
+ closeChapterDetailPopover()
1119
+ return
1120
+ }
1121
+
1122
+ if (openChapterDetailKey.value === key) {
1123
+ closeChapterDetailPopover()
1124
+ return
1125
+ }
1126
+
1127
+ const detail = buildChapterDetail(key)
1128
+ if (!detail) return
1129
+ // 打開詳情前先收起下拉,避免與版本選擇同時彈開
1130
+ closeOpenSelectDropdowns()
1131
+ openChapterDetailKey.value = key
1132
+ openChapterDetail.value = detail
1133
+ // 確保詳情不被滾動區裁切:把當前行滾入可視區
1134
+ void nextTick(() => {
1135
+ const wrap = settingsPanelRef.value?.querySelector(
1136
+ `.chatbot-report-chapter-row-wrap[data-chapter-key="${key}"]`
1137
+ ) as HTMLElement | null
1138
+ wrap?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
1139
+ })
1140
+ }
1141
+
1142
+ function onChapterRowClick(key: string, event: MouseEvent) {
1143
+ if (chapterRowDragMoved) return
1144
+ const target = event.target as HTMLElement | null
1145
+ // 可編輯:點勾選/版本下拉不開詳情
1146
+ // 只讀:下拉已禁用,點版本/生成人/標題區均可打開 popover 查看完整資訊
1147
+ if (props.readonly) {
1148
+ if (target?.closest('.chatbot-report-chapter-row__check')) return
1149
+ toggleChapterDetailPopover(key)
1150
+ return
1151
+ }
1152
+ if (
1153
+ target?.closest(
1154
+ '.chatbot-report-chapter-row__check, .chatbot-report-chapter-row__version, .el-select, .el-select__wrapper'
1155
+ )
1156
+ ) {
1157
+ return
1158
+ }
1159
+ toggleChapterDetailPopover(key)
1160
+ }
1161
+
1162
+ function onDocPointerDown(event: MouseEvent) {
1163
+ if (!openChapterDetailKey.value) return
1164
+ const target = event.target as HTMLElement | null
1165
+ // 點擊版本 / 語言下拉(含 teleport 的 dropdown)→ 收起詳情
1166
+ if (
1167
+ target?.closest(
1168
+ '.chatbot-report-chapter-row__version, .chatbot-report-settings__language, .el-select-dropdown, .el-popper'
1169
+ )
1170
+ ) {
1171
+ closeChapterDetailPopover()
1172
+ return
1173
+ }
1174
+ // 點擊詳情本身不關閉
1175
+ if (target?.closest('.chatbot-report-chapter-title-popover-local')) return
1176
+ // 點擊當前打開詳情的章節行:由 toggle 處理
1177
+ if (target?.closest('.chatbot-report-chapter-row.is-detail-open')) return
1178
+ // 點擊其他章節行:由該行 click 切換;其餘空白區域關閉
1179
+ if (target?.closest('.chatbot-report-chapter-row')) return
1180
+ closeChapterDetailPopover()
1181
+ }
1182
+
1183
+ onMounted(() => {
1184
+ labelResizeObserver = new ResizeObserver(() => remeasureAllChapterLabels())
1185
+ window.addEventListener('resize', remeasureAllChapterLabels)
1186
+ document.addEventListener('mousedown', onDocPointerDown, true)
1187
+ void nextTick(() => {
1188
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1189
+ })
1190
+ // 報告頁掛載:拉報告歷史(失敗置空,不阻斷)
1191
+ if (!props.readonly) {
1192
+ void loadGenAiInstanceHistForReportPage()
1193
+ }
1194
+ })
1195
+
1196
+ onBeforeUnmount(() => {
1197
+ chartAsStreamAbort?.abort()
1198
+ chartAsStreamAbort = null
1199
+ window.removeEventListener('resize', remeasureAllChapterLabels)
1200
+ document.removeEventListener('mousedown', onDocPointerDown, true)
1201
+ labelResizeObserver?.disconnect()
1202
+ labelResizeObserver = null
1203
+ labelElMap.clear()
1204
+ closeChapterDetailPopover()
1205
+ })
1206
+
1207
+ /** AdjustInstance 回寫 existsInstanceVersionNo 後,對照報告歷史選中第一個匹配 card */
1208
+ watch(existsInstanceVersionNo, (versionNo) => {
1209
+ if (props.readonly) return
1210
+ // 有值 = 已通過連貫性檢查 → 顯示結果 / 導出 Word;清空則回落首次生成態
1211
+ syncCoherenceReadyByExistsVersionNo(versionNo)
1212
+ if (suppressHistorySelectByExistsVersion) return
1213
+ if (!reportHistoryItems.value.length) return
1214
+ selectHistoryMatchingExistsVersionNo()
1215
+ })
1216
+
1217
+ /**
1218
+ * existsInstanceVersionNo 有值:已通過連貫性檢查(非首次生成)
1219
+ * 無值:需點「報告連貫性檢查」走 CoherenceCheck
1220
+ */
1221
+ function syncCoherenceReadyByExistsVersionNo(versionNo: string | null | undefined) {
1222
+ const hasVersion = !!String(versionNo ?? '').trim()
1223
+ if (hasVersion) {
1224
+ setCoherenceCheckResultReady(true, coherenceCheckContentText.value)
1225
+ const templateId = selectedTemplate.value?.id
1226
+ const selection = buildCurrentChapterSelection()
1227
+ if (templateId && selection.length) {
1228
+ markCoherenceCheckCompleted(
1229
+ buildGenerationSignature(
1230
+ templateId,
1231
+ selection,
1232
+ draft.language,
1233
+ draft.chapterContentLanguage
1234
+ )
1235
+ )
1236
+ }
1237
+ return
1238
+ }
1239
+ // 無版本號時不強制清 Y(可能剛由 CoherenceCheck 返回 Y、尚未定稿)
1240
+ }
1241
+
1242
+ watch(
1243
+ () => orderedPresets.value.map((item) => item.key).join('|'),
1244
+ () => {
1245
+ void nextTick(() => {
1246
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1247
+ })
1248
+ }
1249
+ )
1250
+
1251
+ watch(settingsOpen, (open) => {
1252
+ if (!open) {
1253
+ closeChapterDetailPopover()
1254
+ return
1255
+ }
1256
+ void nextTick(() => {
1257
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1258
+ })
1259
+ })
1260
+
1261
+ /** AdjustInstance 寫入章節後:生成人由 settingsChapterGenerators 同步 */
1262
+ watch(
1263
+ settingsChapterGenerators,
1264
+ () => {
1265
+ void nextTick(() => {
1266
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1267
+ })
1268
+ },
1269
+ { deep: true }
1270
+ )
1271
+
1272
+ watch(
1273
+ () => props.coherenceSession,
1274
+ (session) => {
1275
+ if (!props.readonly || !session) return
1276
+ applyCoherenceSessionLayout(session)
1277
+ },
1278
+ { immediate: true, deep: true }
1279
+ )
1280
+
1281
+ watch(
1282
+ () => historyPreviewApplyToken.value,
1283
+ () => {
1284
+ applyHistoryPreviewLayout()
1285
+ }
1286
+ )
1287
+
1288
+ watch(
1289
+ [chapterOrder, selectedChapters, chapterVersions],
1290
+ () => {
1291
+ checkHistoryLayoutDrift()
1292
+ // 章節組合變更後需重新連貫性檢查
1293
+ clearCoherenceCheckResultReady()
1294
+ const session = loadCoherenceCheckSession()
1295
+ if (session && String(session.beenFinalizedFlag ?? '').toUpperCase() === 'Y') {
1296
+ saveCoherenceCheckSession({
1297
+ ...session,
1298
+ beenFinalizedFlag: undefined,
1299
+ coherenceCheckContentText: undefined,
1300
+ })
1301
+ }
1302
+ // 勾選 / 拖動 / 切換章節版本 → AdjustInstanceSetting(全部不選不調)
1303
+ void loadAdjustInstanceSettingOnChapterChange()
1304
+ },
1305
+ { deep: true }
1306
+ )
1307
+
1308
+ function applyHistoryPreviewLayout() {
1309
+ if (props.readonly) return
1310
+ const recordId = historyPreviewRecordId.value
1311
+ if (!recordId) return
1312
+
1313
+ const record = findFinalizedReportById(recordId)
1314
+ const templateChapters = selectedTemplate.value?.chapters
1315
+ if (!record || !templateChapters?.length) return
1316
+
1317
+ isApplyingHistoryLayout.value = true
1318
+ suppressAdjustInstanceSetting = true
1319
+ const layout = buildChapterOrderForHistoryRecord(templateChapters, record.chapters, (label) =>
1320
+ UNAVAILABLE_CHAPTER_LABELS.has(label)
1321
+ )
1322
+ chapterPresets.value = layout.presets
1323
+ chapterOrder.value = layout.chapterOrder
1324
+ selectedChapters.value = layout.selectedChapters
1325
+ chapterVersions.value = layout.chapterVersions
1326
+ chapterGenerators.value = Object.fromEntries(layout.presets.map((preset) => [preset.key, '']))
1327
+ isApplyingHistoryLayout.value = false
1328
+ void nextTick(() => {
1329
+ suppressAdjustInstanceSetting = false
1330
+ })
1331
+ }
1332
+
1333
+ function applyCoherenceSessionLayout(session: CoherenceCheckSessionPayload) {
1334
+ const chapters = session.chapters ?? []
1335
+ const presets = chapters.map((chapter, index) => ({
1336
+ key: `coherence-chapter-${index}`,
1337
+ label: String(chapter.label ?? '')
1338
+ .trim()
1339
+ .replace(/^\d+\.\s*/, '') || `章節${index + 1}`,
1340
+ }))
1341
+ const order = presets.map((preset) => preset.key)
1342
+ const selectedMap = Object.fromEntries(presets.map((preset) => [preset.key, true]))
1343
+ const versionMap = Object.fromEntries(
1344
+ presets.map((preset, index) => [preset.key, String(chapters[index]?.version ?? '').trim()])
1345
+ )
1346
+ const generatorMap = Object.fromEntries(
1347
+ presets.map((preset, index) => [
1348
+ preset.key,
1349
+ String(chapters[index]?.generator ?? '').trim(),
1350
+ ])
1351
+ )
1352
+ const optionsMap = Object.fromEntries(
1353
+ presets.map((preset, index) => {
1354
+ const text = versionMap[preset.key] || ''
1355
+ const uploadUsrNameDisplay = generatorMap[preset.key] || ''
1356
+ return [
1357
+ preset.key,
1358
+ text
1359
+ ? [{ text, value: text, selected: true, uploadUsrNameDisplay }]
1360
+ : [],
1361
+ ]
1362
+ })
1363
+ )
1364
+ const paint = () => {
1365
+ chapterPresets.value = presets
1366
+ chapterOrder.value = order
1367
+ selectedChapters.value = selectedMap
1368
+ chapterVersions.value = { ...versionMap }
1369
+ chapterGenerators.value = { ...generatorMap }
1370
+ settingsChapterVersionOptions.value = { ...optionsMap }
1371
+ }
1372
+ paint()
1373
+ void nextTick(paint)
1374
+ }
1375
+
1376
+ function buildCurrentChapterSelection(): ReportChapterSelectionItem[] {
1377
+ return orderedPresets.value
1378
+ .filter((preset) => selectedChapters.value[preset.key] && !isChapterUnavailable(preset))
1379
+ .map((preset) => ({
1380
+ label: preset.label,
1381
+ version: getChapterVersionDisplayText(preset.key),
1382
+ generator: getGeneratorNameChars(preset.key),
1383
+ }))
1384
+ }
1385
+
1386
+ function checkHistoryLayoutDrift() {
1387
+ if (props.readonly) return
1388
+ if (!historyPreviewLayoutBaseline.value || isApplyingHistoryLayout.value) return
1389
+
1390
+ const currentLayout = serializeChapterLayoutFromState(
1391
+ chapterOrder.value,
1392
+ selectedChapters.value,
1393
+ chapterPresets.value,
1394
+ (label) => UNAVAILABLE_CHAPTER_LABELS.has(label)
1395
+ )
1396
+
1397
+ if (currentLayout !== historyPreviewLayoutBaseline.value) {
1398
+ markHistoryPreviewDirty()
1399
+ }
1400
+ }
1401
+
1402
+ function toggleAllChapters() {
1403
+ if (props.readonly) return
1404
+ const selectable = chapterPresets.value.filter((preset) => !isChapterUnavailable(preset))
1405
+ const next = !selectable.every((preset) => selectedChapters.value[preset.key])
1406
+ selectable.forEach((preset) => {
1407
+ selectedChapters.value[preset.key] = next
1408
+ })
1409
+ }
1410
+
1411
+ function onChapterDragStart(event: DragEvent, key: string) {
1412
+ if (props.readonly) {
1413
+ event.preventDefault()
1414
+ return
1415
+ }
1416
+ if (
1417
+ (event.target as HTMLElement).closest(
1418
+ '.chatbot-report-chapter-row__check, .chatbot-report-chapter-row__version, .chatbot-report-chapter-row__label, .el-select'
1419
+ )
1420
+ ) {
1421
+ event.preventDefault()
1422
+ return
1423
+ }
1424
+ chapterRowDragMoved = true
1425
+ dragChapterKey.value = key
1426
+ dragOverChapterKey.value = key
1427
+ }
1428
+
1429
+ function handleChapterDragEnd() {
1430
+ dragChapterKey.value = null
1431
+ dragOverChapterKey.value = null
1432
+ window.setTimeout(() => {
1433
+ chapterRowDragMoved = false
1434
+ }, 0)
1435
+ }
1436
+
1437
+ function handleChapterDrop(targetKey: string) {
1438
+ if (props.readonly) return
1439
+ const draggingKey = dragChapterKey.value
1440
+ if (draggingKey && draggingKey !== targetKey) {
1441
+ const newOrder = [...chapterOrder.value]
1442
+ const fromIdx = newOrder.indexOf(draggingKey)
1443
+ const toIdx = newOrder.indexOf(targetKey)
1444
+ if (fromIdx !== -1 && toIdx !== -1) {
1445
+ newOrder.splice(fromIdx, 1)
1446
+ newOrder.splice(toIdx, 0, draggingKey)
1447
+ chapterOrder.value = newOrder
1448
+ // 釋放後:跳到新位置並從該章起重新列印
1449
+ void nextTick(() => {
1450
+ requestSettingsTypewriterReplay(draggingKey)
1451
+ })
1452
+ }
1453
+ }
1454
+ handleChapterDragEnd()
1455
+ }
1456
+
1457
+ function handleCoherenceCheck() {
1458
+ void loadCoherenceCheckWithApi()
1459
+ }
1460
+
1461
+ async function handleGenerate() {
1462
+ const selection = buildCurrentChapterSelection()
1463
+ if (selection.length === 0) {
1464
+ ElMessage.warning('請先勾選章節')
1465
+ return
1466
+ }
1467
+
1468
+ const templateId = selectedTemplate.value?.id
1469
+ if (!templateId) {
1470
+ ElMessage.warning('請先選擇模板')
1471
+ return
1472
+ }
1473
+
1474
+ // 原有「已生成」校驗(報告歷史章節版本匹配 / 本地定稿簽名)
1475
+ const existingTitle = await findAlreadyGeneratedReportTitle(templateId, selection)
1476
+ if (existingTitle) {
1477
+ ElMessage.info(`所選章節版本已生成報告《${existingTitle}》`)
1478
+ return
1479
+ }
1480
+
1481
+ const signature = buildGenerationSignature(
1482
+ templateId,
1483
+ selection,
1484
+ draft.language,
1485
+ draft.chapterContentLanguage
1486
+ )
1487
+
1488
+ // existsInstanceVersionNo 有值 = 已通過連貫性檢查;否則看本地 signature
1489
+ const coherencePassed =
1490
+ !!String(existsInstanceVersionNo.value ?? '').trim() ||
1491
+ coherenceResultReady.value ||
1492
+ isCoherenceCheckCompleted(signature)
1493
+ if (!coherencePassed) {
1494
+ ElMessage.warning('請先進行報告連貫性檢查後再生成')
1495
+ return
1496
+ }
1497
+
1498
+ try {
1499
+ await ElMessageBox.confirm('檢測到該報告已完成連貫性檢查,確認生成報告?', '提示', {
1500
+ confirmButtonText: '確認',
1501
+ cancelButtonText: '取消',
1502
+ type: 'info',
1503
+ })
1504
+ } catch {
1505
+ return
1506
+ }
1507
+
1508
+ const streamed = await streamChartAsStreamToEditor(selection)
1509
+ if (!streamed) return
1510
+
1511
+ ElMessage.success('已生成')
1512
+ emit('generated', {
1513
+ historyKey: reportHistoryItems.value[0]?.key ?? '',
1514
+ displayTitle: draft.reportName?.trim() || '報告',
1515
+ })
1516
+ }
1517
+
1518
+ const coherenceChecking = ref(false)
1519
+ let chartAsStreamAbort: AbortController | null = null
1520
+ let chartAsStreamSeq = 0
1521
+
1522
+ /** 圖中已勾選章節 + 當前版本(對齊 AdjustInstance.instanceSections) */
1523
+ function buildSelectedInstanceSections(): GenAiAdjustInstanceSection[] {
1524
+ return orderedPresets.value
1525
+ .filter((preset) => selectedChapters.value[preset.key] && !isChapterUnavailable(preset))
1526
+ .map((preset) => {
1527
+ const currentValue = String(chapterVersions.value[preset.key] ?? '').trim()
1528
+ const versions = getChapterVersionOptions(preset.key).map((item) => ({
1529
+ text: item.text,
1530
+ value: item.value,
1531
+ selected: item.value === currentValue,
1532
+ uploadUsrNameDisplay: String(item.uploadUsrNameDisplay ?? '').trim(),
1533
+ }))
1534
+ return {
1535
+ sectionTitle: preset.label,
1536
+ selected: true,
1537
+ createUserName: getGeneratorNameChars(preset.key) || undefined,
1538
+ versions: versions.length
1539
+ ? versions
1540
+ : currentValue
1541
+ ? [
1542
+ {
1543
+ text: getChapterVersionDisplayText(preset.key),
1544
+ value: currentValue,
1545
+ selected: true,
1546
+ uploadUsrNameDisplay: getGeneratorNameChars(preset.key),
1547
+ },
1548
+ ]
1549
+ : [],
1550
+ }
1551
+ })
1552
+ }
1553
+
1554
+ /**
1555
+ * 報告連貫性檢查:AdjustInstance promptType=COHERENCE_CHECK_PROMPT
1556
+ * 傳遞圖中已勾選章節 + 版本,成功後跳轉結果頁打字機展示
1557
+ */
1558
+ async function loadCoherenceCheckWithApi() {
1559
+ const selection = buildCurrentChapterSelection().map((item) => ({
1560
+ label: item.label,
1561
+ version: item.version,
1562
+ generator: String(item.generator ?? '').trim(),
1563
+ }))
1564
+ const versionIdList = buildSelectedVersionIdList()
1565
+ const instanceSections = buildSelectedInstanceSections()
1566
+ const templateId = selectedTemplate.value?.id
1567
+ const instanceId = resolveInstanceId()
1568
+
1569
+ if (!selection.length || !versionIdList.length) {
1570
+ ElMessage.warning('請先勾選章節')
1571
+ return
1572
+ }
1573
+ if (!templateId) {
1574
+ ElMessage.warning('請先選擇模板')
1575
+ return
1576
+ }
1577
+ if (!instanceId) {
1578
+ ElMessage.warning('缺少報告實例,無法進行連貫性檢查')
1579
+ return
1580
+ }
1581
+
1582
+ coherenceChecking.value = true
1583
+ try {
1584
+ const res: any = await genAiReportEditorApi
1585
+ .adjustGenAiInstance({
1586
+ instanceId,
1587
+ promptType: 'COHERENCE_CHECK_PROMPT',
1588
+ versionIdList,
1589
+ instanceSections,
1590
+ })
1591
+ .catch((err) => {
1592
+ console.error('接口异常', err)
1593
+ return null
1594
+ })
1595
+
1596
+ if (!isGenAiResponseOk(res)) {
1597
+ ElMessage.warning(res?.message || '連貫性檢查失敗')
1598
+ return
1599
+ }
1600
+
1601
+ clearCoherenceCheckResultReady()
1602
+ openReportCoherenceCheck(router, {
1603
+ templateId,
1604
+ reportName: draft.reportName,
1605
+ year: draft.year,
1606
+ chapters: selection,
1607
+ chapterContentLanguage: draft.chapterContentLanguage,
1608
+ reportLanguage: draft.language,
1609
+ versionIdList,
1610
+ instanceId,
1611
+ coherenceCheckContentText: pickAdjustInstanceText(res?.data) || undefined,
1612
+ })
1613
+ } finally {
1614
+ coherenceChecking.value = false
1615
+ }
1616
+ }
1617
+
1618
+ /** 點「生成報告」→ chartAsStream SSE,中欄打字機逐塊打印,[DONE] 結束 */
1619
+ async function streamChartAsStreamToEditor(selection: ReportChapterSelectionItem[]) {
1620
+ chartAsStreamAbort?.abort()
1621
+ chartAsStreamAbort = new AbortController()
1622
+ const seq = ++chartAsStreamSeq
1623
+ const signal = chartAsStreamAbort.signal
1624
+
1625
+ const promptDetail = [
1626
+ draft.reportName?.trim(),
1627
+ draft.researchFocus?.trim(),
1628
+ selection.map((item) => item.label).join('\n'),
1629
+ ]
1630
+ .filter(Boolean)
1631
+ .join('\n')
1632
+
1633
+ let started = false
1634
+
1635
+ try {
1636
+ clearChapterChartStreamRefs()
1637
+ for await (const chunk of genAiReportEditorApi.streamChartAsStream(
1638
+ {
1639
+ modelName: String(draft.model || '').trim(),
1640
+ promptDetail: promptDetail || 'who are you?',
1641
+ promptType: 'AI_PROMPT',
1642
+ contentLengthLimit: null,
1643
+ tableInfo: null,
1644
+ chartInfo: null,
1645
+ fileIdList: sourceFiles.value.filter((file) => file.selected).map((file) => file.id),
1646
+ instanceId: resolveInstanceId(),
1647
+ categoryId: 1,
1648
+ temperature: null,
1649
+ maxTokens: null,
1650
+ stream: true,
1651
+ },
1652
+ signal,
1653
+ {
1654
+ //0918:event:REF → SSERefList.push
1655
+ onRefEvent: (ref) => upsertChapterChartStreamRef(ref),
1656
+ }
1657
+ )) {
1658
+ if (seq !== chartAsStreamSeq) return false
1659
+ if (!started) {
1660
+ beginReportGenerateStream()
1661
+ requestSettingsTypewriterReplay('report-generate-stream')
1662
+ started = true
1663
+ }
1664
+ appendReportGenerateStream(chunk)
1665
+ }
1666
+ if (seq !== chartAsStreamSeq) return false
1667
+ if (!started) return false
1668
+ finishReportGenerateStream()
1669
+ return true
1670
+ } catch (err) {
1671
+ if (signal.aborted || seq !== chartAsStreamSeq) return false
1672
+ console.error('接口异常', err)
1673
+ if (started) clearReportGenerateStream()
1674
+ ElMessage.warning(err instanceof Error && err.message.trim() ? err.message : '生成報告失敗')
1675
+ return false
1676
+ }
1677
+ }
1678
+ </script>
1679
+
1680
+ <style scoped>
1681
+ .chatbot-report-panel--settings {
1682
+ --report-accent: #c53355;
1683
+ --report-accent-hover: #a82b48;
1684
+ --report-accent-soft: #fff5f6;
1685
+ --report-border: #ebeef5;
1686
+ --report-card-bg: #f5f5f5;
1687
+ display: flex;
1688
+ flex-direction: column;
1689
+ gap: 15px;
1690
+ min-width: 0;
1691
+ height: 100%;
1692
+ box-sizing: border-box;
1693
+ overflow: visible;
1694
+ }
1695
+
1696
+ .chatbot-report-panel--settings.is-compact {
1697
+ height: auto;
1698
+ align-self: flex-start;
1699
+ }
1700
+
1701
+ /* 報告歷史:與章節歷史相同,展開時吃掉欄內剩餘高度 */
1702
+ .chatbot-report-panel--settings > :deep(.report-collapsible-section:last-child:not(.is-collapsed)) {
1703
+ flex: 1;
1704
+ display: flex;
1705
+ flex-direction: column;
1706
+ min-height: 0;
1707
+ }
1708
+
1709
+ .chatbot-report-panel--settings
1710
+ > :deep(
1711
+ .report-collapsible-section:last-child:not(.is-collapsed) .report-collapsible-section__body
1712
+ ) {
1713
+ flex: 1;
1714
+ display: flex;
1715
+ flex-direction: column;
1716
+ min-height: 0;
1717
+ }
1718
+
1719
+ .chatbot-report-panel--settings :deep(.report-collapsible-section__toggle) {
1720
+ border-bottom: none;
1721
+ }
1722
+
1723
+ .chatbot-report-panel--settings
1724
+ :deep(.report-collapsible-section.is-collapsed .report-collapsible-section__toggle) {
1725
+ padding-bottom: 0;
1726
+ }
1727
+
1728
+ .chatbot-report-panel--settings :deep(.report-collapsible-section__body) {
1729
+ padding-left: 0;
1730
+ padding-right: 0;
1731
+ padding-bottom: 10px;
1732
+ }
1733
+
1734
+ .chatbot-report-panel--settings :deep(.report-collapsible-section__arrow) {
1735
+ border-top-color: var(--report-accent);
1736
+ }
1737
+
1738
+ .chatbot-report-panel--settings.is-readonly {
1739
+ height: 100%;
1740
+ min-height: 0;
1741
+ gap: 0;
1742
+ }
1743
+
1744
+ .chatbot-report-panel--settings.is-readonly.is-compact {
1745
+ height: auto;
1746
+ align-self: flex-start;
1747
+ }
1748
+
1749
+ /* 僅展開時撐滿欄高;收起後隨標題收縮,避免留下空白卡片 */
1750
+ .chatbot-report-panel--settings.is-readonly
1751
+ > :deep(.report-collapsible-section:not(.is-collapsed)) {
1752
+ flex: 1;
1753
+ display: flex;
1754
+ flex-direction: column;
1755
+ min-height: 0;
1756
+ overflow: hidden;
1757
+ }
1758
+
1759
+ .chatbot-report-panel--settings.is-readonly > :deep(.report-collapsible-section.is-collapsed) {
1760
+ flex: 0 0 auto;
1761
+ }
1762
+
1763
+ .chatbot-report-panel--settings.is-readonly
1764
+ > :deep(.report-collapsible-section:not(.is-collapsed) .report-collapsible-section__body) {
1765
+ flex: 1;
1766
+ display: flex;
1767
+ flex-direction: column;
1768
+ min-height: 0;
1769
+ overflow: hidden;
1770
+ }
1771
+
1772
+ .chatbot-report-settings__content {
1773
+ position: relative;
1774
+ display: flex;
1775
+ flex-direction: column;
1776
+ min-width: 0;
1777
+ /* 內容自動撐開,不在設置區內滾動 */
1778
+ min-height: auto;
1779
+ overflow: visible;
1780
+ }
1781
+
1782
+ .chatbot-report-chapter-select {
1783
+ min-height: auto;
1784
+ overflow: visible;
1785
+ --chapter-sort-size: 16px;
1786
+ --chapter-check-size: 14px;
1787
+ --chapter-version-width: 48px;
1788
+ /* 足夠放下 3 個整字,或「兩字...」,避免 overflow 裁出半字 */
1789
+ --chapter-generator-width: 48px;
1790
+ /* 五列共用:拖拽 | 勾選 | 標題(吃剩餘) | 版本 | 生成人 —— 右側略收,標題多一點空間 */
1791
+ --chapter-grid-cols: var(--chapter-sort-size) var(--chapter-check-size) minmax(72px, 1fr)
1792
+ var(--chapter-version-width) var(--chapter-generator-width);
1793
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
1794
+ font-size: 14px;
1795
+ font-weight: 500;
1796
+ color: #303133;
1797
+ }
1798
+
1799
+ .chatbot-report-chapter-header,
1800
+ .chatbot-report-chapter-select-all,
1801
+ .chatbot-report-chapter-row {
1802
+ display: grid;
1803
+ grid-template-columns: var(--chapter-grid-cols);
1804
+ column-gap: 6px;
1805
+ align-items: center;
1806
+ padding: 0;
1807
+ box-sizing: border-box;
1808
+ width: 100%;
1809
+ }
1810
+
1811
+ .chatbot-report-chapter-header {
1812
+ padding-bottom: 6px;
1813
+ }
1814
+
1815
+ .chatbot-report-chapter-header__title,
1816
+ .chatbot-report-chapter-select-all__label,
1817
+ .chatbot-report-chapter-row__label {
1818
+ min-width: 0;
1819
+ text-align: left;
1820
+ justify-self: stretch;
1821
+ }
1822
+
1823
+ .chatbot-report-chapter-header__title {
1824
+ /* 與下方 sort icon 左對齊,橫跨拖拽 + 勾選 + 標題三列 */
1825
+ grid-column: 1 / 4;
1826
+ font-family: inherit;
1827
+ font-size: 14px;
1828
+ font-weight: 500;
1829
+ color: #303133;
1830
+ white-space: nowrap;
1831
+ overflow: hidden;
1832
+ text-overflow: ellipsis;
1833
+ }
1834
+
1835
+ .chatbot-report-chapter-header__col {
1836
+ font-family: inherit;
1837
+ font-size: 14px;
1838
+ font-weight: 500;
1839
+ color: #303133;
1840
+ text-align: left;
1841
+ white-space: nowrap;
1842
+ justify-self: stretch;
1843
+ width: 100%;
1844
+ }
1845
+
1846
+ .chatbot-report-chapter-header__col--version {
1847
+ grid-column: 4;
1848
+ text-align: left;
1849
+ }
1850
+
1851
+ .chatbot-report-chapter-header__col--generator {
1852
+ grid-column: 5;
1853
+ text-align: left;
1854
+ }
1855
+
1856
+ .chatbot-report-chapter-select-all {
1857
+ margin: 8px 0 6px;
1858
+ font-family: inherit;
1859
+ font-size: 14px;
1860
+ font-weight: 500;
1861
+ color: #303133;
1862
+ user-select: none;
1863
+ cursor: pointer;
1864
+ }
1865
+
1866
+ .chatbot-report-chapter-select-all:has(.chatbot-report-chapter-row__check:disabled) {
1867
+ cursor: default;
1868
+ }
1869
+
1870
+ .chatbot-report-chapter-select-all__label {
1871
+ font-family: inherit;
1872
+ font-size: 14px;
1873
+ font-weight: 500;
1874
+ color: #303133;
1875
+ cursor: inherit;
1876
+ }
1877
+
1878
+ .chatbot-report-chapter-row__sort {
1879
+ display: inline-flex;
1880
+ align-items: center;
1881
+ justify-content: center;
1882
+ box-sizing: border-box;
1883
+ width: var(--chapter-sort-size);
1884
+ height: var(--chapter-sort-size);
1885
+ margin: 0;
1886
+ padding: 0;
1887
+ color: #86909c;
1888
+ flex-shrink: 0;
1889
+ cursor: grab;
1890
+ justify-self: start;
1891
+ font-size: var(--chapter-sort-size);
1892
+ }
1893
+
1894
+ .chatbot-report-chapter-row__sort :deep(svg) {
1895
+ width: var(--chapter-sort-size);
1896
+ height: var(--chapter-sort-size);
1897
+ }
1898
+
1899
+ .chatbot-report-chapter-row__sort--placeholder {
1900
+ visibility: hidden;
1901
+ pointer-events: none;
1902
+ }
1903
+
1904
+ .chatbot-report-chapter-row__check {
1905
+ box-sizing: border-box;
1906
+ width: var(--chapter-check-size);
1907
+ height: var(--chapter-check-size);
1908
+ margin: 0;
1909
+ padding: 0;
1910
+ accent-color: var(--report-accent);
1911
+ cursor: pointer;
1912
+ flex-shrink: 0;
1913
+ justify-self: start;
1914
+ }
1915
+
1916
+ .chatbot-report-chapter-row__check--spacer {
1917
+ visibility: hidden;
1918
+ pointer-events: none;
1919
+ appearance: none;
1920
+ border: 0;
1921
+ background: transparent;
1922
+ }
1923
+
1924
+ .chatbot-report-chapter-list {
1925
+ display: flex;
1926
+ flex-direction: column;
1927
+ gap: 2px;
1928
+ overflow: visible;
1929
+ }
1930
+
1931
+ .chatbot-report-chapter-row-wrap {
1932
+ position: relative;
1933
+ z-index: 0;
1934
+ }
1935
+
1936
+ .chatbot-report-chapter-row-wrap.is-popover-open {
1937
+ z-index: 30;
1938
+ }
1939
+
1940
+ .chatbot-report-chapter-row {
1941
+ padding: 6px 0;
1942
+ margin: 0;
1943
+ font-family: inherit;
1944
+ font-size: 14px;
1945
+ font-weight: 500;
1946
+ color: #303133;
1947
+ border-radius: 8px;
1948
+ border: 1px solid transparent;
1949
+ box-sizing: border-box;
1950
+ cursor: grab;
1951
+ user-select: none;
1952
+ align-items: center;
1953
+ transition:
1954
+ background 0.15s ease,
1955
+ box-shadow 0.15s ease,
1956
+ opacity 0.15s ease,
1957
+ border-color 0.15s ease;
1958
+ }
1959
+
1960
+ .chatbot-report-chapter-row__sort,
1961
+ .chatbot-report-chapter-row__check {
1962
+ margin-top: 0;
1963
+ align-self: center;
1964
+ justify-self: start;
1965
+ }
1966
+
1967
+ /* 鼠標移入:灰底 + 圓角(對齊章節列表交互) */
1968
+ .chatbot-report-chapter-row:hover {
1969
+ background: #f5f7fa;
1970
+ border-radius: 8px;
1971
+ }
1972
+
1973
+ /* 拖動中 / 落點 */
1974
+ .chatbot-report-chapter-row.is-dragging {
1975
+ opacity: 0.5;
1976
+ cursor: grabbing;
1977
+ background: #fff;
1978
+ border-color: #c53355;
1979
+ border-radius: 8px;
1980
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
1981
+ }
1982
+
1983
+ .chatbot-report-chapter-row.is-drag-over {
1984
+ background: var(--report-accent-soft);
1985
+ border-color: rgba(197, 51, 85, 0.35);
1986
+ border-radius: 8px;
1987
+ box-shadow: inset 0 0 0 1px rgba(197, 51, 85, 0.35);
1988
+ }
1989
+
1990
+ .chatbot-report-chapter-row__label {
1991
+ min-width: 0;
1992
+ max-width: 100%;
1993
+ overflow: hidden;
1994
+ text-align: left;
1995
+ align-self: center;
1996
+ }
1997
+
1998
+ .chatbot-report-chapter-row__label-text {
1999
+ display: flex;
2000
+ align-items: center;
2001
+ width: 100%;
2002
+ max-width: 100%;
2003
+ min-width: 0;
2004
+ margin: 0;
2005
+ padding: 0;
2006
+ border: none;
2007
+ background: transparent;
2008
+ font-family: inherit;
2009
+ font-size: 14px;
2010
+ line-height: 1.45;
2011
+ font-weight: 500;
2012
+ color: #303133;
2013
+ text-align: left;
2014
+ cursor: default;
2015
+ pointer-events: auto;
2016
+ position: relative;
2017
+ z-index: 1;
2018
+ }
2019
+
2020
+ .chatbot-report-chapter-row__label-main {
2021
+ flex: 1 1 auto;
2022
+ min-width: 0;
2023
+ overflow: hidden;
2024
+ white-space: nowrap;
2025
+ color: #303133;
2026
+ font-family: inherit;
2027
+ font-weight: 500;
2028
+ font-size: 14px;
2029
+ line-height: 1.45;
2030
+ }
2031
+
2032
+ .chatbot-report-chapter-row__label-ellipsis {
2033
+ flex: 0 0 auto;
2034
+ margin-left: 1px;
2035
+ color: #c53355;
2036
+ font-weight: 900;
2037
+ font-size: 15px;
2038
+ line-height: 1.2;
2039
+ letter-spacing: 0;
2040
+ }
2041
+
2042
+ .chatbot-report-chapter-row__label-text.is-truncated {
2043
+ cursor: pointer;
2044
+ }
2045
+
2046
+ .chatbot-report-chapter-row.is-unavailable {
2047
+ cursor: default;
2048
+ }
2049
+
2050
+ .chatbot-report-chapter-row.is-unavailable .chatbot-report-chapter-row__label-text {
2051
+ color: #909399;
2052
+ }
2053
+
2054
+ .chatbot-report-chapter-row__check:disabled {
2055
+ cursor: not-allowed;
2056
+ opacity: 0.45;
2057
+ }
2058
+
2059
+ .chatbot-report-chapter-row__no-version {
2060
+ grid-column: 4 / 6;
2061
+ font-family: inherit;
2062
+ font-size: 14px;
2063
+ font-weight: 500;
2064
+ line-height: 1.45;
2065
+ color: #909399;
2066
+ text-align: left;
2067
+ white-space: nowrap;
2068
+ }
2069
+
2070
+ .chatbot-report-chapter-row__version {
2071
+ width: 100%;
2072
+ min-width: 0;
2073
+ max-width: none;
2074
+ flex-shrink: 0;
2075
+ justify-self: stretch;
2076
+ align-self: center;
2077
+ margin-top: 0;
2078
+ }
2079
+
2080
+ .chatbot-report-chapter-row__version :deep(.el-select__wrapper) {
2081
+ display: flex;
2082
+ align-items: center;
2083
+ justify-content: space-between;
2084
+ gap: 4px;
2085
+ min-height: 24px;
2086
+ padding: 0 6px;
2087
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2088
+ font-size: 14px;
2089
+ font-weight: 500;
2090
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
2091
+ }
2092
+
2093
+ .chatbot-report-chapter-row__version :deep(.el-select__selection) {
2094
+ flex: 1 1 auto;
2095
+ flex-wrap: nowrap;
2096
+ min-width: 0;
2097
+ }
2098
+
2099
+ .chatbot-report-chapter-row__version :deep(.el-select__selected-item) {
2100
+ color: #303133;
2101
+ font-family: inherit;
2102
+ font-size: 14px;
2103
+ font-weight: 500;
2104
+ line-height: 1.3;
2105
+ white-space: nowrap;
2106
+ overflow: hidden;
2107
+ text-overflow: ellipsis;
2108
+ }
2109
+
2110
+ .chatbot-report-chapter-row__version :deep(.el-select__suffix) {
2111
+ flex: 0 0 auto;
2112
+ margin-left: auto;
2113
+ }
2114
+
2115
+ .chatbot-report-chapter-row__version :deep(.el-select__caret) {
2116
+ font-size: 14px;
2117
+ }
2118
+
2119
+ .chatbot-report-chapter-row__version :deep(.el-select__wrapper:hover) {
2120
+ box-shadow: 0 0 0 1px var(--report-accent) inset;
2121
+ }
2122
+
2123
+ .chatbot-report-chapter-row__version :deep(.el-select__wrapper.is-focused) {
2124
+ box-shadow: 0 0 0 1px var(--report-accent) inset;
2125
+ }
2126
+
2127
+ .chatbot-report-chapter-row__generator {
2128
+ display: inline-flex;
2129
+ align-items: center;
2130
+ min-width: 0;
2131
+ width: 100%;
2132
+ max-width: 100%;
2133
+ justify-self: stretch;
2134
+ align-self: center;
2135
+ font-family: inherit;
2136
+ font-size: 14px;
2137
+ font-weight: 500;
2138
+ line-height: 1.45;
2139
+ color: #303133;
2140
+ text-align: left;
2141
+ overflow: hidden;
2142
+ }
2143
+
2144
+ .chatbot-report-chapter-row__generator-main {
2145
+ flex: 0 0 auto;
2146
+ max-width: none;
2147
+ overflow: visible;
2148
+ white-space: nowrap;
2149
+ }
2150
+
2151
+ .chatbot-report-chapter-row__generator-ellipsis {
2152
+ flex: 0 0 auto;
2153
+ margin-left: 1px;
2154
+ color: #c53355;
2155
+ font-weight: 900;
2156
+ font-size: 13px;
2157
+ line-height: 1.2;
2158
+ }
2159
+
2160
+ .chatbot-report-chapter-row.is-ellipsis-clickable {
2161
+ cursor: pointer;
2162
+ }
2163
+
2164
+ .chatbot-report-chapter-row.is-ellipsis-clickable .chatbot-report-chapter-row__label-text,
2165
+ .chatbot-report-chapter-row.is-ellipsis-clickable .chatbot-report-chapter-row__generator {
2166
+ cursor: pointer;
2167
+ }
2168
+
2169
+ .chatbot-report-settings__toolbar {
2170
+ display: flex;
2171
+ justify-content: center;
2172
+ flex-shrink: 0;
2173
+ margin-top: 16px;
2174
+ width: 100%;
2175
+ max-width: 100%;
2176
+ }
2177
+
2178
+ /* 左列:下拉與「報告連貫性檢查」同寬左對齊;右列:「生成報告」與左列按鈕底對齊 */
2179
+ .chatbot-report-settings__toolbar-inner {
2180
+ display: flex;
2181
+ align-items: flex-end;
2182
+ gap: 10px;
2183
+ width: max-content;
2184
+ max-width: 100%;
2185
+ }
2186
+
2187
+ .chatbot-report-settings__col-coherence {
2188
+ display: inline-flex;
2189
+ flex-direction: column;
2190
+ align-items: stretch;
2191
+ gap: 12px;
2192
+ width: max-content;
2193
+ max-width: 100%;
2194
+ }
2195
+
2196
+ .chatbot-report-settings__language {
2197
+ /* 不撐開父級,寬度跟隨「報告連貫性檢查」按鈕 */
2198
+ width: 0;
2199
+ min-width: 100%;
2200
+ max-width: none;
2201
+ flex-shrink: 0;
2202
+ align-self: stretch;
2203
+ }
2204
+
2205
+ .chatbot-report-settings__language :deep(.el-select__wrapper) {
2206
+ width: 100%;
2207
+ min-width: 0;
2208
+ min-height: 32px;
2209
+ padding: 0 10px;
2210
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2211
+ font-size: 16px;
2212
+ font-weight: 500;
2213
+ color: #303133;
2214
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
2215
+ }
2216
+
2217
+ .chatbot-report-settings__language :deep(.el-select__wrapper:hover),
2218
+ .chatbot-report-settings__language :deep(.el-select__wrapper.is-focused) {
2219
+ box-shadow: 0 0 0 1px var(--report-accent) inset;
2220
+ }
2221
+
2222
+ .chatbot-report-settings__language :deep(.el-select__caret) {
2223
+ color: #dcdfe6;
2224
+ }
2225
+
2226
+ .chatbot-report-settings__btn--coherence.el-button {
2227
+ width: max-content;
2228
+ white-space: nowrap;
2229
+ margin-left: 0 !important;
2230
+ }
2231
+
2232
+ .chatbot-report-settings__btn--generate.el-button {
2233
+ margin-left: 0 !important;
2234
+ width: 100px;
2235
+ min-width: 100px;
2236
+ max-width: 100px;
2237
+ padding: 0 8px;
2238
+ font-size: 16px;
2239
+ }
2240
+
2241
+ .chatbot-report-settings__btn--primary.el-button {
2242
+ height: 32px;
2243
+ padding: 0 14px;
2244
+ border: 1px solid var(--report-accent) !important;
2245
+ background: var(--report-accent) !important;
2246
+ color: #fff !important;
2247
+ border-radius: 4px;
2248
+ font-family: var(--gen-ai-font-family, 'Noto Sans', 'Noto Sans TC', sans-serif);
2249
+ font-size: 16px;
2250
+ font-weight: 500;
2251
+ }
2252
+
2253
+ .chatbot-report-settings__btn--primary.el-button:hover:not(.is-disabled) {
2254
+ background: var(--report-accent-hover) !important;
2255
+ border-color: var(--report-accent-hover) !important;
2256
+ color: #fff !important;
2257
+ }
2258
+
2259
+ .chatbot-report-settings__btn--primary.el-button.is-disabled {
2260
+ border-color: #dcdfe6 !important;
2261
+ background: #f5f7fa !important;
2262
+ color: #c0c4cc !important;
2263
+ }
2264
+
2265
+ .chatbot-report-panel--settings.is-readonly :deep(.chatbot-report-chapter-row__sort) {
2266
+ opacity: 0.45;
2267
+ cursor: default;
2268
+ }
2269
+
2270
+ .chatbot-report-panel--settings.is-readonly :deep(.chatbot-report-chapter-row__check:disabled) {
2271
+ cursor: not-allowed;
2272
+ }
2273
+
2274
+ /* 只讀:版本下拉禁用,點擊穿透到行,便於打開詳情 popover */
2275
+ .chatbot-report-panel--settings.is-readonly .chatbot-report-chapter-row__version {
2276
+ pointer-events: none;
2277
+ }
2278
+
2279
+ .chatbot-report-panel--settings.is-readonly .chatbot-report-chapter-row {
2280
+ cursor: pointer;
2281
+ }
2282
+ </style>
2283
+
2284
+ <style>
2285
+ .chatbot-report-panel--settings .el-select-dropdown__item.is-selected {
2286
+ color: #c53355;
2287
+ font-weight: 600;
2288
+ }
2289
+
2290
+ .chatbot-report-panel--settings .el-select-dropdown__item.is-hovering {
2291
+ background-color: #fff5f6;
2292
+ }
2293
+
2294
+ /* 章節詳情:浮動在當前行下方,absolute 不佔文檔流空間 */
2295
+ .chatbot-report-chapter-title-popover-local {
2296
+ position: absolute;
2297
+ top: calc(100% + 2px);
2298
+ left: 0;
2299
+ right: 0;
2300
+ z-index: 40;
2301
+ width: 100%;
2302
+ max-width: 100%;
2303
+ margin: 0;
2304
+ padding: 12px 14px;
2305
+ border: 1px solid #ebeef5;
2306
+ border-radius: 8px;
2307
+ background: #fff;
2308
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
2309
+ box-sizing: border-box;
2310
+ }
2311
+
2312
+ /* 檢查頁只讀:避免被父級 overflow 裁切詳情 */
2313
+ .chatbot-report-panel--settings.is-readonly
2314
+ .chatbot-report-chapter-row-wrap.is-popover-open
2315
+ .chatbot-report-chapter-title-popover-local {
2316
+ z-index: 60;
2317
+ }
2318
+
2319
+ .chatbot-report-chapter-title-popover__title {
2320
+ margin: 0 0 10px;
2321
+ color: #303133;
2322
+ font-size: 14px;
2323
+ font-weight: 600;
2324
+ line-height: 1.4;
2325
+ }
2326
+
2327
+ .chatbot-report-chapter-title-popover__body {
2328
+ margin: 0;
2329
+ color: #303133;
2330
+ font-size: 13px;
2331
+ line-height: 1.5;
2332
+ word-break: break-word;
2333
+ }
2334
+
2335
+ .chatbot-report-chapter-detail-row {
2336
+ display: grid;
2337
+ grid-template-columns: 64px minmax(0, 1fr);
2338
+ column-gap: 10px;
2339
+ align-items: start;
2340
+ }
2341
+
2342
+ .chatbot-report-chapter-detail-row + .chatbot-report-chapter-detail-row {
2343
+ margin-top: 10px;
2344
+ }
2345
+
2346
+ .chatbot-report-chapter-detail-row__label {
2347
+ color: #909399;
2348
+ font-size: 12px;
2349
+ font-weight: 600;
2350
+ line-height: 1.5;
2351
+ white-space: nowrap;
2352
+ }
2353
+
2354
+ .chatbot-report-chapter-detail-row__value {
2355
+ min-width: 0;
2356
+ color: #303133;
2357
+ font-size: 13px;
2358
+ line-height: 1.5;
2359
+ word-break: break-word;
2360
+ }
2361
+ </style>