aegon-lv82 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 (148) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +44 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/build-report/reportEditor.ts +526 -0
  5. package/src/api/modules/build-report/reportPermission.ts +64 -0
  6. package/src/api/modules/index.ts +69 -0
  7. package/src/api/modules/industry/green_mortgage.ts +42 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +124 -0
  10. package/src/assets/sample-access-icon.png +0 -0
  11. package/src/assets/sample-pie-chart.png +0 -0
  12. package/src/assets/vue.svg +1 -0
  13. package/src/components/CapsuleScrollbar.vue +7 -0
  14. package/src/components/Export/ExcelExport.vue +592 -0
  15. package/src/components/Export/ExcelExport2.vue +494 -0
  16. package/src/components/Export/ExcelExport3.vue +342 -0
  17. package/src/components/Export/ExcelExport4.vue +665 -0
  18. package/src/components/Export/excelExport.js +547 -0
  19. package/src/components/Export/excelExport.ts +551 -0
  20. package/src/components/GEN-AI/index.vue +140 -0
  21. package/src/components/GEN-AI/index1.vue +422 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +548 -0
  24. package/src/components/GEN-AI/index3.vue +605 -0
  25. package/src/components/GEN-AI/index4.vue +609 -0
  26. package/src/components/GEN-AI/index5.vue +558 -0
  27. package/src/components/GEN-AI/index6.vue +634 -0
  28. package/src/components/GEN-AI/index7.vue +688 -0
  29. package/src/components/GEN-AI/index8.vue +458 -0
  30. package/src/components/GEN-AI/index9.vue +1217 -0
  31. package/src/components/GEN-AI/types.ts +12 -0
  32. package/src/components/GEN-AI/utils.ts +42 -0
  33. package/src/components/HelloWorld.vue +41 -0
  34. package/src/components/PageCard.vue +124 -0
  35. package/src/components/PageHeader.vue +32 -0
  36. package/src/components/backup/index5 copy.vue +556 -0
  37. package/src/components/backup/index5.vue +620 -0
  38. package/src/components/backup/index9 copy.vue +1029 -0
  39. package/src/components/backup/index9-pro.vue +1065 -0
  40. package/src/components/backup/index9.vue +1057 -0
  41. package/src/directives/btnLoading.ts +455 -0
  42. package/src/directives/debounce copy.ts +670 -0
  43. package/src/directives/debounce.ts +98 -0
  44. package/src/directives/index.ts +25 -0
  45. package/src/layouts/MainLayout.vue +129 -0
  46. package/src/main.ts +76 -0
  47. package/src/router/index.ts +142 -0
  48. package/src/router/menus.ts +28 -0
  49. package/src/store/modules/gen-ai/index.ts +1 -0
  50. package/src/store/modules/gen-ai/reportBuild.ts +37 -0
  51. package/src/style.css +79 -0
  52. package/src/styles/report-config-popover.css +39 -0
  53. package/src/styles/report-icon-btn.css +19 -0
  54. package/src/test/mock.ts +101 -0
  55. package/src/test/test1.vue +402 -0
  56. package/src/test/test2.vue +1689 -0
  57. package/src/types/gen-ai/build-report/reportEditor.ts +684 -0
  58. package/src/utils/function.ts +46 -0
  59. package/src/utils/gen-ai-navigation.ts +1 -0
  60. package/src/utils/gen-ai-scroll.ts +1 -0
  61. package/src/utils/openDataLoaderWordExport.ts +33 -0
  62. package/src/utils/pageScrollbar.ts +115 -0
  63. package/src/utils/randomTranscode.ts +87 -0
  64. package/src/utils/reportPdfExport.ts +44 -0
  65. package/src/views/Blank.vue +68 -0
  66. package/src/views/Home.vue +29 -0
  67. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  68. package/src/views/TemplateCenter/data.d.ts +10 -0
  69. package/src/views/TemplateCenter/index.vue +1128 -0
  70. package/src/views/TemplateCenter/service.ts +69 -0
  71. package/src/views/gen-ai/build-report/chartStreamRefs.ts +174 -0
  72. package/src/views/gen-ai/build-report/coherence-check/index.vue +559 -0
  73. package/src/views/gen-ai/build-report/components/ReportChapterWorkspace.vue +4258 -0
  74. package/src/views/gen-ai/build-report/components/ReportConfigPanel.vue +727 -0
  75. package/src/views/gen-ai/build-report/components/ReportGenerateSettingsPanel.vue +2361 -0
  76. package/src/views/gen-ai/build-report/components/ReportGeneratingPanel.vue +1765 -0
  77. package/src/views/gen-ai/build-report/components/ReportPromptPanel.vue +614 -0
  78. package/src/views/gen-ai/build-report/components/ReportSourceFileSummary.vue +656 -0
  79. package/src/views/gen-ai/build-report/components/ReportSourcesPanel.vue +1327 -0
  80. package/src/views/gen-ai/build-report/components/ReportTemplateDetail.vue +191 -0
  81. package/src/views/gen-ai/build-report/components/ReportTemplateEditorFormPanel.vue +667 -0
  82. package/src/views/gen-ai/build-report/components/ReportTemplatePicker.vue +302 -0
  83. package/src/views/gen-ai/build-report/components/ReportWorkflowSidebar.vue +148 -0
  84. package/src/views/gen-ai/build-report/components/paramsSetting.vue +886 -0
  85. package/src/views/gen-ai/build-report/components/reportEdit.vue +2283 -0
  86. package/src/views/gen-ai/build-report/index.vue +856 -0
  87. package/src/views/gen-ai/build-report/report-domain.ts +1133 -0
  88. package/src/views/gen-ai/build-report/report-template-data.ts +295 -0
  89. package/src/views/gen-ai/build-report/useReportWorkflow.ts +1408 -0
  90. package/src/views/gen-ai/common/assets/NotoSans-SC.woff2 +0 -0
  91. package/src/views/gen-ai/common/assets/NotoSans-TC.woff2 +0 -0
  92. package/src/views/gen-ai/common/assets/NotoSans.woff2 +0 -0
  93. package/src/views/gen-ai/common/assets/fonts.css +278 -0
  94. package/src/views/gen-ai/common/assets/pagination.css +163 -0
  95. package/src/views/gen-ai/common/directives/btnLoading.ts +455 -0
  96. package/src/views/gen-ai/common/directives/debounce.ts +98 -0
  97. package/src/views/gen-ai/common/directives/index.ts +30 -0
  98. package/src/views/gen-ai/common/en.md +417 -0
  99. package/src/views/gen-ai/common/index.ts +36 -0
  100. package/src/views/gen-ai/common/types/component-expose.ts +9 -0
  101. package/src/views/gen-ai/common/utils/api-res-status.ts +7 -0
  102. package/src/views/gen-ai/common/utils/navigation.ts +59 -0
  103. package/src/views/gen-ai/common/utils/remote-request.ts +45 -0
  104. package/src/views/gen-ai/common/utils/scroll.ts +577 -0
  105. package/src/views/gen-ai/components/AIExport/docx-export.ts +950 -0
  106. package/src/views/gen-ai/components/AIExport/docxExport.ts +2566 -0
  107. package/src/views/gen-ai/components/AIExport/index.ts +52 -0
  108. package/src/views/gen-ai/components/AIExport/parseMarkdownHtml.ts +649 -0
  109. package/src/views/gen-ai/components/CapsuleScrollbar.vue +149 -0
  110. package/src/views/gen-ai/components/ChapterTitleScroll.vue +468 -0
  111. package/src/views/gen-ai/components/EntryHeroNavCards.vue +379 -0
  112. package/src/views/gen-ai/components/GenAiPagination.vue +52 -0
  113. package/src/views/gen-ai/components/ModelCard.vue +1114 -0
  114. package/src/views/gen-ai/components/Preview/FilePreviewPage.vue +947 -0
  115. package/src/views/gen-ai/components/Preview/PreviewCapsuleScroll.vue +205 -0
  116. package/src/views/gen-ai/components/Preview/PreviewOutlineTree.vue +148 -0
  117. package/src/views/gen-ai/components/Preview/index.ts +11 -0
  118. package/src/views/gen-ai/components/Preview/preview.ts +722 -0
  119. package/src/views/gen-ai/components/PromptField.vue +505 -0
  120. package/src/views/gen-ai/components/RecentReportsTable.vue +974 -0
  121. package/src/views/gen-ai/components/ReportCollapsibleSection.vue +172 -0
  122. package/src/views/gen-ai/components/ReportHistoryList.vue +637 -0
  123. package/src/views/gen-ai/components/ReportSectionConfirmPopover.vue +251 -0
  124. package/src/views/gen-ai/components/editor/ChapterTiptapEditor.vue +3139 -0
  125. package/src/views/gen-ai/components/editor/StructureBlockView.vue +441 -0
  126. package/src/views/gen-ai/components/editor/TiptapToolbar.vue +1036 -0
  127. package/src/views/gen-ai/components/editor/TypewriterDocEditor.vue +398 -0
  128. package/src/views/gen-ai/components/editor/_StructureOrgNode.vue +326 -0
  129. package/src/views/gen-ai/components/editor/index.ts +12 -0
  130. package/src/views/gen-ai/components/editor/parseMarkdownHtml.ts +667 -0
  131. package/src/views/gen-ai/gen-entry/api/index.ts +32 -0
  132. package/src/views/gen-ai/gen-entry/index.vue +287 -0
  133. package/src/views/gen-ai/gen-entry/types/index.ts +4 -0
  134. package/src/views/gen-ai/i.mark +2 -0
  135. package/src/views/gen-ai/management-center/index.vue +1815 -0
  136. package/src/views/gen-ai/management-center/mockData.ts +248 -0
  137. package/src/views/gen-ai/management-center/types/index.ts +37 -0
  138. package/src/views/gen-ai/model-manager/TemplateEditorFormPanel.vue +661 -0
  139. package/src/views/gen-ai/model-manager/api/index.ts +32 -0
  140. package/src/views/gen-ai/model-manager/api/mockApi.ts +33 -0
  141. package/src/views/gen-ai/model-manager/index.vue +1583 -0
  142. package/src/views/gen-ai/model-manager/mockData.ts +177 -0
  143. package/src/views/gen-ai/model-manager/template-detail-shared.ts +154 -0
  144. package/src/views/gen-ai/model-manager/template-editor.vue +345 -0
  145. package/src/views/gen-ai/model-manager/types/index.ts +40 -0
  146. package/src/views/gen-ai/model-manager/utils.ts +25 -0
  147. package/src/views/greenMortgage.vue +1834 -0
  148. package/src/vite-env.d.ts +46 -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
+ setAdjustInstanceContents(contentList)
640
+ const firstKey = settingsChapterOrder.value[0]
641
+ if (fp !== lastAdjustContentsFingerprint) {
642
+ lastAdjustContentsFingerprint = fp
643
+ if (firstKey) requestSettingsTypewriterReplay(firstKey)
644
+ } else if (orderRestoredToDrag && firstKey) {
645
+ requestSettingsTypewriterReplay(firstKey)
646
+ }
647
+ }
648
+
649
+ markAdjustInstanceSettingSynced(buildSelectedVersionIdList())
650
+ } finally {
651
+ void nextTick(() => {
652
+ suppressAdjustInstanceSetting = false
653
+ })
654
+ }
655
+
656
+ console.log('调整报告设置', {
657
+ versionIdList: params.versionIdList,
658
+ existsInstanceVersionNo: existsInstanceVersionNo.value,
659
+ sections: sectionList.length,
660
+ contents: contentList.length,
661
+ fileList,
662
+ })
663
+ return { ok: true as const }
664
+ } catch (err) {
665
+ console.error('接口异常', err)
666
+ return { ok: false as const }
667
+ }
668
+ }
669
+
670
+ /** 勾選 / 拖動 / 切換章節版本後調用;全部不選不調用(原有佈局聯動 + 資訊源反顯) */
671
+ async function loadAdjustInstanceSettingOnChapterChange() {
672
+ if (props.readonly) return false
673
+ if (
674
+ suppressAdjustInstanceSetting ||
675
+ suppressAdjustInstanceSettingWatch.value ||
676
+ isApplyingHistoryLayout.value
677
+ ) {
678
+ return false
679
+ }
680
+ // 尚無接口版本選項時不調(避免模板 mock V1..V6)
681
+ if (!Object.keys(settingsChapterVersionOptions.value).length) return false
682
+
683
+ const versionIdList = buildSelectedVersionIdList()
684
+ if (!versionIdList.length) {
685
+ console.log('[report editor] 未勾選章節,跳過 AdjustInstanceSetting')
686
+ return false
687
+ }
688
+ // 與上次回寫結果一致 → 跳過(打斷回寫觸發的 watch 循環)
689
+ if (isAdjustInstanceSettingSynced(versionIdList)) return false
690
+
691
+ const instanceId = resolveInstanceId()
692
+ if (!instanceId) {
693
+ console.warn('[report editor] 無 instanceId,AdjustInstanceSetting 跳過')
694
+ return false
695
+ }
696
+
697
+ const seq = ++adjustInstanceSettingSeq
698
+ const result = await adjustGenAiInstanceSetting({ instanceId, versionIdList })
699
+ if (seq !== adjustInstanceSettingSeq) return false
700
+ return result.ok
701
+ }
702
+
703
+ /**
704
+ * 報告定稿 FinalInstance
705
+ * 生成報告設置頁點「生成報告」後調用;參數同 AdjustInstanceSetting
706
+ */
707
+ const finalGenAiInstance = async (params: { instanceId: string; versionIdList: string[] }) => {
708
+ const res: any = await genAiReportEditorApi
709
+ .finalGenAiInstance({
710
+ instanceId: params.instanceId,
711
+ versionIdList: params.versionIdList,
712
+ })
713
+ .catch((err) => {
714
+ console.error('接口异常', err)
715
+ return null
716
+ })
717
+
718
+ try {
719
+ if (!isGenAiResponseOk(res)) {
720
+ console.warn('[report editor] FinalInstance', res?.message || '接口异常')
721
+ return { ok: false as const, message: String(res?.message || '報告定稿失敗') }
722
+ }
723
+ console.log('报告定稿 FinalInstance', {
724
+ versionIdList: params.versionIdList,
725
+ data: res?.data,
726
+ })
727
+ return { ok: true as const, message: String(res?.message || '') }
728
+ } catch (err) {
729
+ console.error('接口异常', err)
730
+ return { ok: false as const, message: '報告定稿失敗' }
731
+ }
732
+ }
733
+
734
+ /** 點「生成報告」→ FinalInstance;失敗不走本地定稿 */
735
+ async function loadFinalInstanceOnGenerate() {
736
+ const versionIdList = buildSelectedVersionIdList()
737
+ if (!versionIdList.length) {
738
+ ElMessage.warning('請先勾選章節')
739
+ return false
740
+ }
741
+
742
+ const instanceId = resolveInstanceId()
743
+ if (!instanceId) {
744
+ console.warn('[report editor] 無 instanceId,FinalInstance 跳過')
745
+ ElMessage.warning('缺少報告實例,無法生成')
746
+ return false
747
+ }
748
+
749
+ const result = await finalGenAiInstance({ instanceId, versionIdList })
750
+ if (!result.ok) {
751
+ ElMessage.warning(result.message || '報告定稿失敗')
752
+ return false
753
+ }
754
+ // 定稿成功後刷新報告歷史
755
+ await loadGenAiInstanceHistForReportPage()
756
+ return true
757
+ }
758
+
759
+ /** zh-Hans → CHS(簡體),zh-Hant → CHT(繁體) */
760
+ function mapChapterContentLanguageToTargetLang(
761
+ lang: ChapterContentLanguage
762
+ ): GenAiSimTradTargetLang {
763
+ return lang === 'zh-Hans' ? 'CHS' : 'CHT'
764
+ }
765
+
766
+ /**
767
+ * 繁簡轉換 SimTradTransfer
768
+ * 生成報告設置「語言」下拉切換後調用;回寫中欄 instanceContents
769
+ */
770
+ const simTradTransferGenAiInstance = async (params: {
771
+ targetLang: GenAiSimTradTargetLang
772
+ instanceContents: Array<{ sectionTitle: string; sectionContent: string }>
773
+ }) => {
774
+ const res: any = await genAiReportEditorApi
775
+ .simTradTransferGenAiInstance({
776
+ targetLang: params.targetLang,
777
+ instanceContents: params.instanceContents,
778
+ })
779
+ .catch((err) => {
780
+ console.error('接口异常', err)
781
+ return null
782
+ })
783
+
784
+ try {
785
+ if (!isGenAiResponseOk(res)) {
786
+ console.warn('[report editor] SimTradTransfer', res?.message || '接口异常')
787
+ return { ok: false as const }
788
+ }
789
+ const raw = res?.data
790
+ const list = Array.isArray(raw?.instanceContents)
791
+ ? raw.instanceContents
792
+ : Array.isArray(raw)
793
+ ? raw
794
+ : []
795
+ setAdjustInstanceContents(
796
+ list.map((item: { sectionTitle?: string; sectionContent?: string }) => ({
797
+ sectionTitle: String(item?.sectionTitle ?? ''),
798
+ sectionContent: String(item?.sectionContent ?? ''),
799
+ }))
800
+ )
801
+ lastAdjustContentsFingerprint = fingerprintAdjustContents(adjustInstanceContents.value)
802
+ console.log('繁简转换 SimTradTransfer', {
803
+ targetLang: params.targetLang,
804
+ contents: adjustInstanceContents.value,
805
+ })
806
+ return { ok: true as const }
807
+ } catch (err) {
808
+ console.error('接口异常', err)
809
+ return { ok: false as const }
810
+ }
811
+ }
812
+
813
+ /** 語言下拉:先走原有 draft 語言切換,再調繁簡轉換;無正文時僅保留本地語言(標題 i18n) */
814
+ async function loadSimTradTransferOnLanguageChange(lang: ChapterContentLanguage) {
815
+ if (props.readonly) return false
816
+ const contents = adjustInstanceContents.value
817
+ if (!contents.length) {
818
+ console.log('[report editor] 無 instanceContents,跳過 SimTradTransfer(保留本地語言切換)')
819
+ return false
820
+ }
821
+
822
+ const targetLang = mapChapterContentLanguageToTargetLang(lang)
823
+ const seq = ++simTradTransferSeq
824
+ const result = await simTradTransferGenAiInstance({
825
+ targetLang,
826
+ instanceContents: contents.map((item) => ({
827
+ sectionTitle: item.sectionTitle,
828
+ sectionContent: item.sectionContent,
829
+ })),
830
+ })
831
+ if (seq !== simTradTransferSeq) return false
832
+ if (!result.ok) {
833
+ ElMessage.warning('繁簡轉換失敗')
834
+ return false
835
+ }
836
+ // 原有中欄重播:內容已換
837
+ const firstKey = settingsChapterOrder.value[0]
838
+ if (firstKey) requestSettingsTypewriterReplay(firstKey)
839
+ return true
840
+ }
841
+
842
+ // —— report editor api end ——
843
+
844
+ function handleSelectHistory(item: ReportHistoryItem) {
845
+ activeHistoryKey.value = item.key
846
+ const voIndex = reportHistoryItems.value.findIndex((entry) => entry.key === item.key)
847
+ const vo = voIndex >= 0 ? genAiInstanceVersionVOList.value[voIndex] : undefined
848
+ if (vo) {
849
+ // 切換報告歷史 → AdjustInstanceSetting 刷新:生成報告設置 + 中欄 editor + 資訊源
850
+ const versionIdList = splitCommaIds(vo.sectionVersionIdList)
851
+ const instanceId = String(vo.instanceId ?? '').trim() || resolveInstanceId()
852
+ console.log('選中報告歷史', {
853
+ instanceId,
854
+ sectionIds: splitCommaIds(vo.sectionIdList),
855
+ sectionVersionIds: versionIdList,
856
+ instanceVersionNo: vo.instanceVersionNo,
857
+ vo,
858
+ })
859
+ if (instanceId && versionIdList.length) {
860
+ void loadAdjustInstanceSettingFromHistoryCard({ instanceId, versionIdList })
861
+ } else {
862
+ ElMessage.warning('該歷史缺少版本信息,無法刷新報告設置')
863
+ }
864
+ resetHistoryPreviewState()
865
+ return
866
+ }
867
+ if (!item.recordId) {
868
+ resetHistoryPreviewState()
869
+ return
870
+ }
871
+ // 本地定稿歷史:原有預覽邏輯
872
+ selectHistoryReport(item.recordId)
873
+ }
874
+
875
+ /**
876
+ * 點擊報告歷史 card → AdjustInstanceSetting 刷新頁面(資訊源等)
877
+ * 參數取自該 card 的 instanceId + sectionVersionIdList
878
+ */
879
+ async function loadAdjustInstanceSettingFromHistoryCard(params: {
880
+ instanceId: string
881
+ versionIdList: string[]
882
+ }) {
883
+ if (props.readonly) return false
884
+ const selectedKey = activeHistoryKey.value
885
+ suppressAdjustInstanceSetting = true
886
+ suppressHistorySelectByExistsVersion = true
887
+ const seq = ++adjustInstanceSettingSeq
888
+ const result = await adjustGenAiInstanceSetting(params)
889
+ if (seq !== adjustInstanceSettingSeq) return false
890
+ // 保持用戶點選的 card
891
+ if (selectedKey) activeHistoryKey.value = selectedKey
892
+ void nextTick(() => {
893
+ suppressAdjustInstanceSetting = false
894
+ suppressHistorySelectByExistsVersion = false
895
+ })
896
+ return result.ok
897
+ }
898
+
899
+ /** 中欄打字進度經 workflow 通知;右側自行滾動列表行,不依賴中欄 DOM */
900
+ watch(settingsTypingChapterKey, (key) => {
901
+ if (!key) return
902
+ void nextTick(() => {
903
+ const row = document.querySelector(
904
+ `.chatbot-report-chapter-row-wrap[data-chapter-key="${CSS.escape(key)}"]`
905
+ ) as HTMLElement | null
906
+ row?.scrollIntoView({ block: 'nearest', behavior: 'smooth', inline: 'nearest' })
907
+ })
908
+ })
909
+
910
+ watch(historyScrollTargetKey, async (key) => {
911
+ if (!key) return
912
+ historyOpen.value = true
913
+ await nextTick()
914
+ clearHistoryScrollTarget()
915
+ })
916
+
917
+ /** 無報告歷史時收起面板,避免空內容佔位 */
918
+ watch(
919
+ reportHistoryItems,
920
+ (items) => {
921
+ if (!items.length) historyOpen.value = false
922
+ },
923
+ { deep: true }
924
+ )
925
+
926
+ const chapterContentLanguageOptions = CHAPTER_CONTENT_LANGUAGE_OPTIONS
927
+
928
+ const showChapterLanguageSelect = computed(() => shouldShowChapterLanguageSelect(draft.language))
929
+
930
+ const chapterContentLanguage = computed({
931
+ get: () => draft.chapterContentLanguage,
932
+ set: (value: ChapterContentLanguage) => {
933
+ if (draft.chapterContentLanguage === value) return
934
+ patchDraft({ chapterContentLanguage: value })
935
+ // 語言下拉:CHS/CHT 繁簡轉換
936
+ void loadSimTradTransferOnLanguageChange(value)
937
+ },
938
+ })
939
+
940
+ const unavailableVersionLabel = computed(() =>
941
+ translateUnavailableVersionLabel(draft.language, 'zh-Hant')
942
+ )
943
+
944
+ const chapterPresets = settingsChapterPresets
945
+ const selectedChapters = settingsSelectedChapters
946
+ /** 與 AdjustInstance 共用:value=UUID(或回落 V1..V6) */
947
+ const chapterVersions = settingsChapterVersions
948
+ const chapterGenerators = settingsChapterGenerators
949
+ const chapterOrder = settingsChapterOrder
950
+ const dragChapterKey = ref<string | null>(null)
951
+ const dragOverChapterKey = ref<string | null>(null)
952
+ const isApplyingHistoryLayout = ref(false)
953
+ let chapterRowDragMoved = false
954
+
955
+ /** 章節版本下拉:僅接口 versions;無則空(不回落 V1..V6 mock) */
956
+ function getChapterVersionOptions(key: string) {
957
+ if (props.readonly) {
958
+ const text = String(chapterVersions.value[key] ?? '').trim()
959
+ const uploadUsrNameDisplay = String(chapterGenerators.value[key] ?? '').trim()
960
+ return text
961
+ ? [{ text, value: text, selected: true, uploadUsrNameDisplay }]
962
+ : []
963
+ }
964
+ return settingsChapterVersionOptions.value[key] ?? []
965
+ }
966
+
967
+ /** 詳情彈層展示 text,而非 UUID */
968
+ function getChapterVersionDisplayText(key: string) {
969
+ const value = chapterVersions.value[key]
970
+ const opt = getChapterVersionOptions(key).find((item) => item.value === value)
971
+ return opt?.text || value || '—'
972
+ }
973
+
974
+ /**
975
+ * 生成人展示:≤3 字完整顯示;>3 字只顯示前 4 字 + ...(對齊圖中 LOCA...)
976
+ * 完整資訊在詳情彈層
977
+ */
978
+ const GENERATOR_NAME_FULL_MAX = 3
979
+ const GENERATOR_NAME_TRUNCATED_CHARS = 4
980
+
981
+ function getGeneratorNameChars(key: string) {
982
+ const name = getChapterVersionOptions(key).find(
983
+ (item) => item.value == chapterVersions.value[key]
984
+ )?.uploadUsrNameDisplay
985
+ return String(name ?? '').trim()
986
+ }
987
+
988
+ function isGeneratorNameTruncated(key: string) {
989
+ return Array.from(getGeneratorNameChars(key)).length > GENERATOR_NAME_FULL_MAX
990
+ }
991
+
992
+ function formatGeneratorDisplayName(key: string) {
993
+ const chars = Array.from(getGeneratorNameChars(key))
994
+ if (!chars.length) return ''
995
+ if (chars.length <= GENERATOR_NAME_FULL_MAX) return chars.join('')
996
+ return chars.slice(0, GENERATOR_NAME_TRUNCATED_CHARS).join('')
997
+ }
998
+
999
+ const orderedPresets = computed(() =>
1000
+ chapterOrder.value
1001
+ .map((key) => chapterPresets.value.find((preset) => preset.key === key))
1002
+ .filter((preset): preset is ChapterPreset => !!preset)
1003
+ )
1004
+
1005
+ const allChaptersSelected = computed(() => {
1006
+ const selectable = chapterPresets.value.filter((preset) => !isChapterUnavailable(preset))
1007
+ return selectable.length > 0 && selectable.every((preset) => selectedChapters.value[preset.key])
1008
+ })
1009
+
1010
+ function isChapterUnavailable(preset: ChapterPreset) {
1011
+ return UNAVAILABLE_CHAPTER_LABELS.has(preset.label)
1012
+ }
1013
+
1014
+ function formatChapterDisplayTitle(_index: number, label: string) {
1015
+ return translateChapterLabel(label, draft.language, 'zh-Hant')
1016
+ }
1017
+
1018
+ /** 章節標題是否溢出;完整詳情掛在當前章節行下方 */
1019
+ const labelOverflowMap = reactive<Record<string, boolean>>({})
1020
+ const labelElMap = new Map<string, HTMLElement>()
1021
+ const openChapterDetailKey = ref<string | null>(null)
1022
+ const openChapterDetail = ref<{ title: string; version: string; generator: string } | null>(null)
1023
+ const settingsPanelRef = ref<HTMLElement | null>(null)
1024
+ let labelResizeObserver: ResizeObserver | null = null
1025
+
1026
+ function measureChapterLabelOverflow(key: string) {
1027
+ const el = labelElMap.get(key)
1028
+ if (!el) {
1029
+ labelOverflowMap[key] = false
1030
+ return
1031
+ }
1032
+ const main = el.querySelector('.chatbot-report-chapter-row__label-main') as HTMLElement | null
1033
+ if (!main) {
1034
+ labelOverflowMap[key] = false
1035
+ return
1036
+ }
1037
+ labelOverflowMap[key] = main.scrollWidth > main.clientWidth + 1
1038
+ }
1039
+
1040
+ function bindChapterLabelEl(key: string, el: unknown) {
1041
+ const prev = labelElMap.get(key)
1042
+ if (prev && labelResizeObserver) labelResizeObserver.unobserve(prev)
1043
+
1044
+ const node = el as HTMLElement | null
1045
+ if (!node) {
1046
+ labelElMap.delete(key)
1047
+ labelOverflowMap[key] = false
1048
+ return
1049
+ }
1050
+
1051
+ labelElMap.set(key, node)
1052
+ labelResizeObserver?.observe(node)
1053
+ const shell = node.closest('.chatbot-report-chapter-row__label')
1054
+ if (shell) labelResizeObserver?.observe(shell)
1055
+ void nextTick(() => measureChapterLabelOverflow(key))
1056
+ }
1057
+
1058
+ function remeasureAllChapterLabels() {
1059
+ for (const key of labelElMap.keys()) measureChapterLabelOverflow(key)
1060
+ }
1061
+
1062
+ function buildChapterDetail(key: string) {
1063
+ const presetIndex = orderedPresets.value.findIndex((item) => item.key === key)
1064
+ const preset = orderedPresets.value[presetIndex]
1065
+ if (!preset) return null
1066
+ return {
1067
+ title: formatChapterDisplayTitle(presetIndex, preset.label),
1068
+ version: isChapterUnavailable(preset)
1069
+ ? unavailableVersionLabel.value
1070
+ : getChapterVersionDisplayText(preset.key),
1071
+ generator: getGeneratorNameChars(key) || '',
1072
+ }
1073
+ }
1074
+
1075
+ function closeChapterDetailPopover() {
1076
+ openChapterDetailKey.value = null
1077
+ openChapterDetail.value = null
1078
+ }
1079
+
1080
+ /** 關閉面板內已展開的 el-select 下拉(與詳情彈層互斥) */
1081
+ function closeOpenSelectDropdowns() {
1082
+ const panel = settingsPanelRef.value
1083
+ if (!panel) return
1084
+ panel
1085
+ .querySelectorAll<HTMLElement>('.el-select__wrapper[aria-expanded="true"]')
1086
+ .forEach((node) => {
1087
+ node.blur()
1088
+ node.click()
1089
+ })
1090
+ // teleported 下拉:Escape 再兜底關閉
1091
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
1092
+ }
1093
+
1094
+ /** 點選版本 / 語言下拉:立刻收起詳情 */
1095
+ function onVersionSelectMouseDown() {
1096
+ closeChapterDetailPopover()
1097
+ }
1098
+
1099
+ /** 打開版本 / 語言下拉時關閉章節詳情 */
1100
+ function onChapterVersionVisibleChange(open: boolean) {
1101
+ if (open) closeChapterDetailPopover()
1102
+ }
1103
+
1104
+ function hasChapterRowEllipsis(preset: ChapterPreset) {
1105
+ const titleOverflow = !!labelOverflowMap[preset.key]
1106
+ const generatorOverflow =
1107
+ !isChapterUnavailable(preset) && isGeneratorNameTruncated(preset.key)
1108
+ return titleOverflow || generatorOverflow
1109
+ }
1110
+
1111
+ function toggleChapterDetailPopover(key: string) {
1112
+ const preset = orderedPresets.value.find((item) => item.key === key)
1113
+ if (!preset) {
1114
+ closeChapterDetailPopover()
1115
+ return
1116
+ }
1117
+ // 可編輯:僅標題/生成人截斷時可開詳情;只讀檢查頁:任意行可查看完整資訊
1118
+ if (!props.readonly && !hasChapterRowEllipsis(preset)) {
1119
+ closeChapterDetailPopover()
1120
+ return
1121
+ }
1122
+
1123
+ if (openChapterDetailKey.value === key) {
1124
+ closeChapterDetailPopover()
1125
+ return
1126
+ }
1127
+
1128
+ const detail = buildChapterDetail(key)
1129
+ if (!detail) return
1130
+ // 打開詳情前先收起下拉,避免與版本選擇同時彈開
1131
+ closeOpenSelectDropdowns()
1132
+ openChapterDetailKey.value = key
1133
+ openChapterDetail.value = detail
1134
+ // 確保詳情不被滾動區裁切:把當前行滾入可視區
1135
+ void nextTick(() => {
1136
+ const wrap = settingsPanelRef.value?.querySelector(
1137
+ `.chatbot-report-chapter-row-wrap[data-chapter-key="${key}"]`
1138
+ ) as HTMLElement | null
1139
+ wrap?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
1140
+ })
1141
+ }
1142
+
1143
+ function onChapterRowClick(key: string, event: MouseEvent) {
1144
+ if (chapterRowDragMoved) return
1145
+ const target = event.target as HTMLElement | null
1146
+ // 可編輯:點勾選/版本下拉不開詳情
1147
+ // 只讀:下拉已禁用,點版本/生成人/標題區均可打開 popover 查看完整資訊
1148
+ if (props.readonly) {
1149
+ if (target?.closest('.chatbot-report-chapter-row__check')) return
1150
+ toggleChapterDetailPopover(key)
1151
+ return
1152
+ }
1153
+ if (
1154
+ target?.closest(
1155
+ '.chatbot-report-chapter-row__check, .chatbot-report-chapter-row__version, .el-select, .el-select__wrapper'
1156
+ )
1157
+ ) {
1158
+ return
1159
+ }
1160
+ toggleChapterDetailPopover(key)
1161
+ }
1162
+
1163
+ function onDocPointerDown(event: MouseEvent) {
1164
+ if (!openChapterDetailKey.value) return
1165
+ const target = event.target as HTMLElement | null
1166
+ // 點擊版本 / 語言下拉(含 teleport 的 dropdown)→ 收起詳情
1167
+ if (
1168
+ target?.closest(
1169
+ '.chatbot-report-chapter-row__version, .chatbot-report-settings__language, .el-select-dropdown, .el-popper'
1170
+ )
1171
+ ) {
1172
+ closeChapterDetailPopover()
1173
+ return
1174
+ }
1175
+ // 點擊詳情本身不關閉
1176
+ if (target?.closest('.chatbot-report-chapter-title-popover-local')) return
1177
+ // 點擊當前打開詳情的章節行:由 toggle 處理
1178
+ if (target?.closest('.chatbot-report-chapter-row.is-detail-open')) return
1179
+ // 點擊其他章節行:由該行 click 切換;其餘空白區域關閉
1180
+ if (target?.closest('.chatbot-report-chapter-row')) return
1181
+ closeChapterDetailPopover()
1182
+ }
1183
+
1184
+ onMounted(() => {
1185
+ labelResizeObserver = new ResizeObserver(() => remeasureAllChapterLabels())
1186
+ window.addEventListener('resize', remeasureAllChapterLabels)
1187
+ document.addEventListener('mousedown', onDocPointerDown, true)
1188
+ void nextTick(() => {
1189
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1190
+ })
1191
+ // 報告頁掛載:拉報告歷史(失敗置空,不阻斷)
1192
+ if (!props.readonly) {
1193
+ void loadGenAiInstanceHistForReportPage()
1194
+ }
1195
+ })
1196
+
1197
+ onBeforeUnmount(() => {
1198
+ chartAsStreamAbort?.abort()
1199
+ chartAsStreamAbort = null
1200
+ window.removeEventListener('resize', remeasureAllChapterLabels)
1201
+ document.removeEventListener('mousedown', onDocPointerDown, true)
1202
+ labelResizeObserver?.disconnect()
1203
+ labelResizeObserver = null
1204
+ labelElMap.clear()
1205
+ closeChapterDetailPopover()
1206
+ })
1207
+
1208
+ /** AdjustInstance 回寫 existsInstanceVersionNo 後,對照報告歷史選中第一個匹配 card */
1209
+ watch(existsInstanceVersionNo, (versionNo) => {
1210
+ if (props.readonly) return
1211
+ // 有值 = 已通過連貫性檢查 → 顯示結果 / 導出 Word;清空則回落首次生成態
1212
+ syncCoherenceReadyByExistsVersionNo(versionNo)
1213
+ if (suppressHistorySelectByExistsVersion) return
1214
+ if (!reportHistoryItems.value.length) return
1215
+ selectHistoryMatchingExistsVersionNo()
1216
+ })
1217
+
1218
+ /**
1219
+ * existsInstanceVersionNo 有值:已通過連貫性檢查(非首次生成)
1220
+ * 無值:需點「報告連貫性檢查」走 CoherenceCheck
1221
+ */
1222
+ function syncCoherenceReadyByExistsVersionNo(versionNo: string | null | undefined) {
1223
+ const hasVersion = !!String(versionNo ?? '').trim()
1224
+ if (hasVersion) {
1225
+ setCoherenceCheckResultReady(true, coherenceCheckContentText.value)
1226
+ const templateId = selectedTemplate.value?.id
1227
+ const selection = buildCurrentChapterSelection()
1228
+ if (templateId && selection.length) {
1229
+ markCoherenceCheckCompleted(
1230
+ buildGenerationSignature(
1231
+ templateId,
1232
+ selection,
1233
+ draft.language,
1234
+ draft.chapterContentLanguage
1235
+ )
1236
+ )
1237
+ }
1238
+ return
1239
+ }
1240
+ // 無版本號時不強制清 Y(可能剛由 CoherenceCheck 返回 Y、尚未定稿)
1241
+ }
1242
+
1243
+ watch(
1244
+ () => orderedPresets.value.map((item) => item.key).join('|'),
1245
+ () => {
1246
+ void nextTick(() => {
1247
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1248
+ })
1249
+ }
1250
+ )
1251
+
1252
+ watch(settingsOpen, (open) => {
1253
+ if (!open) {
1254
+ closeChapterDetailPopover()
1255
+ return
1256
+ }
1257
+ void nextTick(() => {
1258
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1259
+ })
1260
+ })
1261
+
1262
+ /** AdjustInstance 寫入章節後:生成人由 settingsChapterGenerators 同步 */
1263
+ watch(
1264
+ settingsChapterGenerators,
1265
+ () => {
1266
+ void nextTick(() => {
1267
+ requestAnimationFrame(() => remeasureAllChapterLabels())
1268
+ })
1269
+ },
1270
+ { deep: true }
1271
+ )
1272
+
1273
+ watch(
1274
+ () => props.coherenceSession,
1275
+ (session) => {
1276
+ if (!props.readonly || !session) return
1277
+ applyCoherenceSessionLayout(session)
1278
+ },
1279
+ { immediate: true, deep: true }
1280
+ )
1281
+
1282
+ watch(
1283
+ () => historyPreviewApplyToken.value,
1284
+ () => {
1285
+ applyHistoryPreviewLayout()
1286
+ }
1287
+ )
1288
+
1289
+ watch(
1290
+ [chapterOrder, selectedChapters, chapterVersions],
1291
+ () => {
1292
+ checkHistoryLayoutDrift()
1293
+ // 章節組合變更後需重新連貫性檢查
1294
+ clearCoherenceCheckResultReady()
1295
+ const session = loadCoherenceCheckSession()
1296
+ if (session && String(session.beenFinalizedFlag ?? '').toUpperCase() === 'Y') {
1297
+ saveCoherenceCheckSession({
1298
+ ...session,
1299
+ beenFinalizedFlag: undefined,
1300
+ coherenceCheckContentText: undefined,
1301
+ })
1302
+ }
1303
+ // 勾選 / 拖動 / 切換章節版本 → AdjustInstanceSetting(全部不選不調)
1304
+ void loadAdjustInstanceSettingOnChapterChange()
1305
+ },
1306
+ { deep: true }
1307
+ )
1308
+
1309
+ function applyHistoryPreviewLayout() {
1310
+ if (props.readonly) return
1311
+ const recordId = historyPreviewRecordId.value
1312
+ if (!recordId) return
1313
+
1314
+ const record = findFinalizedReportById(recordId)
1315
+ const templateChapters = selectedTemplate.value?.chapters
1316
+ if (!record || !templateChapters?.length) return
1317
+
1318
+ isApplyingHistoryLayout.value = true
1319
+ suppressAdjustInstanceSetting = true
1320
+ const layout = buildChapterOrderForHistoryRecord(templateChapters, record.chapters, (label) =>
1321
+ UNAVAILABLE_CHAPTER_LABELS.has(label)
1322
+ )
1323
+ chapterPresets.value = layout.presets
1324
+ chapterOrder.value = layout.chapterOrder
1325
+ selectedChapters.value = layout.selectedChapters
1326
+ chapterVersions.value = layout.chapterVersions
1327
+ chapterGenerators.value = Object.fromEntries(layout.presets.map((preset) => [preset.key, '']))
1328
+ isApplyingHistoryLayout.value = false
1329
+ void nextTick(() => {
1330
+ suppressAdjustInstanceSetting = false
1331
+ })
1332
+ }
1333
+
1334
+ function applyCoherenceSessionLayout(session: CoherenceCheckSessionPayload) {
1335
+ const chapters = session.chapters ?? []
1336
+ const presets = chapters.map((chapter, index) => ({
1337
+ key: `coherence-chapter-${index}`,
1338
+ label: String(chapter.label ?? '')
1339
+ .trim()
1340
+ .replace(/^\d+\.\s*/, '') || `章節${index + 1}`,
1341
+ }))
1342
+ const order = presets.map((preset) => preset.key)
1343
+ const selectedMap = Object.fromEntries(presets.map((preset) => [preset.key, true]))
1344
+ const versionMap = Object.fromEntries(
1345
+ presets.map((preset, index) => [preset.key, String(chapters[index]?.version ?? '').trim()])
1346
+ )
1347
+ const generatorMap = Object.fromEntries(
1348
+ presets.map((preset, index) => [
1349
+ preset.key,
1350
+ String(chapters[index]?.generator ?? '').trim(),
1351
+ ])
1352
+ )
1353
+ const optionsMap = Object.fromEntries(
1354
+ presets.map((preset, index) => {
1355
+ const text = versionMap[preset.key] || ''
1356
+ const uploadUsrNameDisplay = generatorMap[preset.key] || ''
1357
+ return [
1358
+ preset.key,
1359
+ text
1360
+ ? [{ text, value: text, selected: true, uploadUsrNameDisplay }]
1361
+ : [],
1362
+ ]
1363
+ })
1364
+ )
1365
+ const paint = () => {
1366
+ chapterPresets.value = presets
1367
+ chapterOrder.value = order
1368
+ selectedChapters.value = selectedMap
1369
+ chapterVersions.value = { ...versionMap }
1370
+ chapterGenerators.value = { ...generatorMap }
1371
+ settingsChapterVersionOptions.value = { ...optionsMap }
1372
+ }
1373
+ paint()
1374
+ void nextTick(paint)
1375
+ }
1376
+
1377
+ function buildCurrentChapterSelection(): ReportChapterSelectionItem[] {
1378
+ return orderedPresets.value
1379
+ .filter((preset) => selectedChapters.value[preset.key] && !isChapterUnavailable(preset))
1380
+ .map((preset) => ({
1381
+ label: preset.label,
1382
+ version: getChapterVersionDisplayText(preset.key),
1383
+ generator: getGeneratorNameChars(preset.key),
1384
+ }))
1385
+ }
1386
+
1387
+ function checkHistoryLayoutDrift() {
1388
+ if (props.readonly) return
1389
+ if (!historyPreviewLayoutBaseline.value || isApplyingHistoryLayout.value) return
1390
+
1391
+ const currentLayout = serializeChapterLayoutFromState(
1392
+ chapterOrder.value,
1393
+ selectedChapters.value,
1394
+ chapterPresets.value,
1395
+ (label) => UNAVAILABLE_CHAPTER_LABELS.has(label)
1396
+ )
1397
+
1398
+ if (currentLayout !== historyPreviewLayoutBaseline.value) {
1399
+ markHistoryPreviewDirty()
1400
+ }
1401
+ }
1402
+
1403
+ function toggleAllChapters() {
1404
+ if (props.readonly) return
1405
+ const selectable = chapterPresets.value.filter((preset) => !isChapterUnavailable(preset))
1406
+ const next = !selectable.every((preset) => selectedChapters.value[preset.key])
1407
+ selectable.forEach((preset) => {
1408
+ selectedChapters.value[preset.key] = next
1409
+ })
1410
+ }
1411
+
1412
+ function onChapterDragStart(event: DragEvent, key: string) {
1413
+ if (props.readonly) {
1414
+ event.preventDefault()
1415
+ return
1416
+ }
1417
+ if (
1418
+ (event.target as HTMLElement).closest(
1419
+ '.chatbot-report-chapter-row__check, .chatbot-report-chapter-row__version, .chatbot-report-chapter-row__label, .el-select'
1420
+ )
1421
+ ) {
1422
+ event.preventDefault()
1423
+ return
1424
+ }
1425
+ chapterRowDragMoved = true
1426
+ dragChapterKey.value = key
1427
+ dragOverChapterKey.value = key
1428
+ }
1429
+
1430
+ function handleChapterDragEnd() {
1431
+ dragChapterKey.value = null
1432
+ dragOverChapterKey.value = null
1433
+ window.setTimeout(() => {
1434
+ chapterRowDragMoved = false
1435
+ }, 0)
1436
+ }
1437
+
1438
+ function handleChapterDrop(targetKey: string) {
1439
+ if (props.readonly) return
1440
+ const draggingKey = dragChapterKey.value
1441
+ if (draggingKey && draggingKey !== targetKey) {
1442
+ const newOrder = [...chapterOrder.value]
1443
+ const fromIdx = newOrder.indexOf(draggingKey)
1444
+ const toIdx = newOrder.indexOf(targetKey)
1445
+ if (fromIdx !== -1 && toIdx !== -1) {
1446
+ newOrder.splice(fromIdx, 1)
1447
+ newOrder.splice(toIdx, 0, draggingKey)
1448
+ chapterOrder.value = newOrder
1449
+ // 釋放後:跳到新位置並從該章起重新列印
1450
+ void nextTick(() => {
1451
+ requestSettingsTypewriterReplay(draggingKey)
1452
+ })
1453
+ }
1454
+ }
1455
+ handleChapterDragEnd()
1456
+ }
1457
+
1458
+ function handleCoherenceCheck() {
1459
+ void loadCoherenceCheckWithApi()
1460
+ }
1461
+
1462
+ async function handleGenerate() {
1463
+ const selection = buildCurrentChapterSelection()
1464
+ if (selection.length === 0) {
1465
+ ElMessage.warning('請先勾選章節')
1466
+ return
1467
+ }
1468
+
1469
+ const templateId = selectedTemplate.value?.id
1470
+ if (!templateId) {
1471
+ ElMessage.warning('請先選擇模板')
1472
+ return
1473
+ }
1474
+
1475
+ // 原有「已生成」校驗(報告歷史章節版本匹配 / 本地定稿簽名)
1476
+ const existingTitle = await findAlreadyGeneratedReportTitle(templateId, selection)
1477
+ if (existingTitle) {
1478
+ ElMessage.info(`所選章節版本已生成報告《${existingTitle}》`)
1479
+ return
1480
+ }
1481
+
1482
+ const signature = buildGenerationSignature(
1483
+ templateId,
1484
+ selection,
1485
+ draft.language,
1486
+ draft.chapterContentLanguage
1487
+ )
1488
+
1489
+ // existsInstanceVersionNo 有值 = 已通過連貫性檢查;否則看本地 signature
1490
+ const coherencePassed =
1491
+ !!String(existsInstanceVersionNo.value ?? '').trim() ||
1492
+ coherenceResultReady.value ||
1493
+ isCoherenceCheckCompleted(signature)
1494
+ if (!coherencePassed) {
1495
+ ElMessage.warning('請先進行報告連貫性檢查後再生成')
1496
+ return
1497
+ }
1498
+
1499
+ try {
1500
+ await ElMessageBox.confirm('檢測到該報告已完成連貫性檢查,確認生成報告?', '提示', {
1501
+ confirmButtonText: '確認',
1502
+ cancelButtonText: '取消',
1503
+ type: 'info',
1504
+ })
1505
+ } catch {
1506
+ return
1507
+ }
1508
+
1509
+ const streamed = await streamChartAsStreamToEditor(selection)
1510
+ if (!streamed) return
1511
+
1512
+ ElMessage.success('已生成')
1513
+ emit('generated', {
1514
+ historyKey: reportHistoryItems.value[0]?.key ?? '',
1515
+ displayTitle: draft.reportName?.trim() || '報告',
1516
+ })
1517
+ }
1518
+
1519
+ const coherenceChecking = ref(false)
1520
+ let chartAsStreamAbort: AbortController | null = null
1521
+ let chartAsStreamSeq = 0
1522
+
1523
+ /** 圖中已勾選章節 + 當前版本(對齊 AdjustInstance.instanceSections) */
1524
+ function buildSelectedInstanceSections(): GenAiAdjustInstanceSection[] {
1525
+ return orderedPresets.value
1526
+ .filter((preset) => selectedChapters.value[preset.key] && !isChapterUnavailable(preset))
1527
+ .map((preset) => {
1528
+ const currentValue = String(chapterVersions.value[preset.key] ?? '').trim()
1529
+ const versions = getChapterVersionOptions(preset.key).map((item) => ({
1530
+ text: item.text,
1531
+ value: item.value,
1532
+ selected: item.value === currentValue,
1533
+ uploadUsrNameDisplay: String(item.uploadUsrNameDisplay ?? '').trim(),
1534
+ }))
1535
+ return {
1536
+ sectionTitle: preset.label,
1537
+ selected: true,
1538
+ createUserName: getGeneratorNameChars(preset.key) || undefined,
1539
+ versions: versions.length
1540
+ ? versions
1541
+ : currentValue
1542
+ ? [
1543
+ {
1544
+ text: getChapterVersionDisplayText(preset.key),
1545
+ value: currentValue,
1546
+ selected: true,
1547
+ uploadUsrNameDisplay: getGeneratorNameChars(preset.key),
1548
+ },
1549
+ ]
1550
+ : [],
1551
+ }
1552
+ })
1553
+ }
1554
+
1555
+ /**
1556
+ * 報告連貫性檢查:AdjustInstance promptType=COHERENCE_CHECK_PROMPT
1557
+ * 傳遞圖中已勾選章節 + 版本,成功後跳轉結果頁打字機展示
1558
+ */
1559
+ async function loadCoherenceCheckWithApi() {
1560
+ const selection = buildCurrentChapterSelection().map((item) => ({
1561
+ label: item.label,
1562
+ version: item.version,
1563
+ generator: String(item.generator ?? '').trim(),
1564
+ }))
1565
+ const versionIdList = buildSelectedVersionIdList()
1566
+ const instanceSections = buildSelectedInstanceSections()
1567
+ const templateId = selectedTemplate.value?.id
1568
+ const instanceId = resolveInstanceId()
1569
+
1570
+ if (!selection.length || !versionIdList.length) {
1571
+ ElMessage.warning('請先勾選章節')
1572
+ return
1573
+ }
1574
+ if (!templateId) {
1575
+ ElMessage.warning('請先選擇模板')
1576
+ return
1577
+ }
1578
+ if (!instanceId) {
1579
+ ElMessage.warning('缺少報告實例,無法進行連貫性檢查')
1580
+ return
1581
+ }
1582
+
1583
+ coherenceChecking.value = true
1584
+ try {
1585
+ const res: any = await genAiReportEditorApi
1586
+ .adjustGenAiInstance({
1587
+ instanceId,
1588
+ promptType: 'COHERENCE_CHECK_PROMPT',
1589
+ versionIdList,
1590
+ instanceSections,
1591
+ })
1592
+ .catch((err) => {
1593
+ console.error('接口异常', err)
1594
+ return null
1595
+ })
1596
+
1597
+ if (!isGenAiResponseOk(res)) {
1598
+ ElMessage.warning(res?.message || '連貫性檢查失敗')
1599
+ return
1600
+ }
1601
+
1602
+ clearCoherenceCheckResultReady()
1603
+ openReportCoherenceCheck(router, {
1604
+ templateId,
1605
+ reportName: draft.reportName,
1606
+ year: draft.year,
1607
+ chapters: selection,
1608
+ chapterContentLanguage: draft.chapterContentLanguage,
1609
+ reportLanguage: draft.language,
1610
+ versionIdList,
1611
+ instanceId,
1612
+ coherenceCheckContentText: pickAdjustInstanceText(res?.data) || undefined,
1613
+ })
1614
+ } finally {
1615
+ coherenceChecking.value = false
1616
+ }
1617
+ }
1618
+
1619
+ /** 點「生成報告」→ chartAsStream SSE,中欄打字機逐塊打印,[DONE] 結束 */
1620
+ async function streamChartAsStreamToEditor(selection: ReportChapterSelectionItem[]) {
1621
+ chartAsStreamAbort?.abort()
1622
+ chartAsStreamAbort = new AbortController()
1623
+ const seq = ++chartAsStreamSeq
1624
+ const signal = chartAsStreamAbort.signal
1625
+
1626
+ const promptDetail = [
1627
+ draft.reportName?.trim(),
1628
+ draft.researchFocus?.trim(),
1629
+ selection.map((item) => item.label).join('\n'),
1630
+ ]
1631
+ .filter(Boolean)
1632
+ .join('\n')
1633
+
1634
+ let started = false
1635
+
1636
+ try {
1637
+ clearChapterChartStreamRefs()
1638
+ for await (const chunk of genAiReportEditorApi.streamChartAsStream(
1639
+ {
1640
+ modelName: String(draft.model || '').trim(),
1641
+ promptDetail: promptDetail || 'who are you?',
1642
+ promptType: 'AI_PROMPT',
1643
+ contentLengthLimit: null,
1644
+ tableInfo: null,
1645
+ chartInfo: null,
1646
+ fileIdList: sourceFiles.value.filter((file) => file.selected).map((file) => file.id),
1647
+ instanceId: resolveInstanceId(),
1648
+ categoryId: 1,
1649
+ temperature: null,
1650
+ maxTokens: null,
1651
+ stream: true,
1652
+ },
1653
+ signal,
1654
+ {
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>