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,1408 @@
1
+ import { computed, nextTick, reactive, ref, watch } from 'vue'
2
+ import {
3
+ clearChartStreamRefs,
4
+ upsertChartStreamRef,
5
+ upsertChartStreamRefsFromSources,
6
+ } from './chartStreamRefs'
7
+ import {
8
+ buildReportHistoryChapterBodies,
9
+ findFinalizedReportById,
10
+ serializeSelectedChapterLabels,
11
+ stripChapterNumberPrefix,
12
+ } from './report-domain'
13
+ import type { ReportTemplateCard } from './report-template-data'
14
+
15
+ /**
16
+ * 只負責流程控制與路由跳轉。
17
+ * 接口類 API 調用寫在當前調用組件內,禁止寫在本文件。
18
+ */
19
+
20
+ export const FILE_SIZE_LIMIT_KB = 100 * 1024
21
+ export const SOURCE_VISIBLE_LIMIT = 20
22
+
23
+ /** 章節歷史刷新:由 ReportChapterWorkspace 註冊 loadGenAiSectionVersionList */
24
+ type ChapterHistoryReloader = () => Promise<boolean>
25
+ let chapterHistoryReloader: ChapterHistoryReloader | null = null
26
+
27
+ export function registerChapterHistoryReloader(fn: ChapterHistoryReloader | null) {
28
+ chapterHistoryReloader = fn
29
+ }
30
+
31
+ export async function reloadActiveChapterHistory() {
32
+ if (!chapterHistoryReloader) return false
33
+ return chapterHistoryReloader()
34
+ }
35
+
36
+ export type SourceFilterTab = 'all' | 'selected' | 'unselected'
37
+
38
+ export interface PendingFile {
39
+ id: string
40
+ name: string
41
+ sizeKb: number
42
+ }
43
+
44
+ export interface SourceFile {
45
+ id: string
46
+ name: string
47
+ selected: boolean
48
+ /** 可選直鏈;預覽/下載優先使用 */
49
+ url?: string
50
+ }
51
+
52
+ export const MOCK_SOURCE_FILES: SourceFile[] = []
53
+
54
+ export const MOCK_PENDING_FILES: PendingFile[] = []
55
+
56
+ export type ReportEditorRole = 'initiator' | 'editor'
57
+
58
+ export interface ReportEditor {
59
+ id: string
60
+ name: string
61
+ employeeId: string
62
+ department: string
63
+ role: ReportEditorRole
64
+ }
65
+
66
+ export interface UmsEditorCandidate {
67
+ id: string
68
+ name: string
69
+ employeeId: string
70
+ department: string
71
+ }
72
+
73
+ export const CURRENT_USER_ID = '8000121'
74
+
75
+ /** 編輯者列表改由接口 / 用戶添加寫入;不再預填本地假人員 */
76
+ export const MOCK_REPORT_EDITORS: ReportEditor[] = []
77
+
78
+ export const MOCK_UMS_EDITOR_USERS: UmsEditorCandidate[] = []
79
+
80
+ export type ReportModelStep = 'select' | 'detail' | 'generating' | 'prompt'
81
+ export type PromptRightPanel = 'prompt' | 'settings'
82
+ /** Prompt AI+ / 新增章節 AI+ → 打開智能化生成 Prompt 彈窗 */
83
+ export type AiPromptDialogSource = 'prompt-panel' | 'add-chapter'
84
+
85
+ export interface ReportHistoryItem {
86
+ key: string
87
+ title: string
88
+ author: string
89
+ finalizedAt: string
90
+ recordId?: string
91
+ /** 存為草稿後插入章節歷史首行的草稿卡片 */
92
+ kind?: 'draft' | 'history'
93
+ }
94
+
95
+ export const CHAPTER_DRAFT_HISTORY_KEY = 'chapter-history-draft'
96
+ export const CHAPTER_DRAFT_HISTORY_TITLE = '您有未生成章節內容的草稿...'
97
+
98
+ export interface PromptChapterForm {
99
+ prompt: string
100
+ enableBodyContent: boolean
101
+ bodyWordLimit: number
102
+ enableTable: boolean
103
+ tableTitle: string
104
+ tableRowLabels: string
105
+ tableColumnLabels: string
106
+ tableContent: string
107
+ enableSourceChart: boolean
108
+ chartDescription: string
109
+ }
110
+
111
+ export function createDefaultPromptChapterForm(): PromptChapterForm {
112
+ return {
113
+ prompt: '',
114
+ enableBodyContent: false,
115
+ bodyWordLimit: 500,
116
+ enableTable: true,
117
+ tableTitle: '',
118
+ tableRowLabels: '',
119
+ tableColumnLabels: '',
120
+ tableContent: '',
121
+ enableSourceChart: true,
122
+ chartDescription: '',
123
+ }
124
+ }
125
+
126
+ export interface ReportDraft {
127
+ industry: string
128
+ researchFocus: string
129
+ year: string
130
+ reportName: string
131
+ language: string
132
+ chapterContentLanguage: 'zh-Hant' | 'zh-Hans'
133
+ wordsPerChapter: string
134
+ model: string
135
+ searchMode: string
136
+ weight: number
137
+ rerank: boolean
138
+ rerankTopN: number
139
+ rerankThreshold: number
140
+ sourceCount: number
141
+ }
142
+
143
+ const STORAGE_KEY = 'gen-ai-report-workflow-draft'
144
+ const TEMPLATE_STORAGE_KEY = 'gen-ai-report-workflow-template'
145
+ const SOURCES_STORAGE_KEY = 'gen-ai-report-workflow-sources'
146
+ const EDITORS_STORAGE_KEY = 'gen-ai-report-workflow-editors'
147
+ const BODIES_STORAGE_KEY = 'gen-ai-report-workflow-generated-bodies'
148
+ const TITLES_STORAGE_KEY = 'gen-ai-report-workflow-generated-titles'
149
+ const SETTINGS_STORAGE_KEY = 'gen-ai-report-workflow-settings'
150
+ const PROMPT_FORM_STORAGE_KEY = 'gen-ai-report-workflow-prompt-form'
151
+ /** 一次性清掉舊版表格/圖表預填默認值 */
152
+ const PROMPT_TABLE_DEFAULTS_CLEARED_KEY = 'gen-ai-report-prompt-table-defaults-cleared-v1'
153
+ const GENERATING_STORAGE_KEY = 'gen-ai-report-generating-session'
154
+
155
+ const draft = reactive<ReportDraft>({
156
+ industry: '',
157
+ researchFocus: '',
158
+ year: String(new Date().getFullYear()),
159
+ reportName: '',
160
+ language: 'zh-Hant',
161
+ chapterContentLanguage: 'zh-Hant',
162
+ wordsPerChapter: '',
163
+ model: 'gpt-666',
164
+ searchMode: 'Hybrid Search',
165
+ weight: 0.5,
166
+ rerank: true,
167
+ rerankTopN: 5,
168
+ rerankThreshold: 0.5,
169
+ sourceCount: 0,
170
+ })
171
+
172
+ const selectedTemplate = ref<ReportTemplateCard | null>(null)
173
+ const modelStep = ref<ReportModelStep>('select')
174
+ const editViewOpen = ref(false)
175
+ /** 強制重掛整個 ReportEdit 組件(一鍵生成 / 進入編輯) */
176
+ const reportEditMountKey = ref(0)
177
+ /** 主動跳轉鎖:避免路由 watch / sync 在中途把 modelStep 打回舊值 */
178
+ const reportEditNavLocked = ref(false)
179
+ const reportEditNavTarget = ref<ReportModelStep | null>(null)
180
+ /** 鎖定期目標右側欄;解鎖時強制寫回,避免 remount 後丟 settings */
181
+ const reportEditNavRightPanel = ref<PromptRightPanel | null>(null)
182
+ const promptRightPanel = ref<PromptRightPanel>('prompt')
183
+
184
+ /**
185
+ * 右側欄 / 中欄工具列 / 中欄正文 — 各自獨立開關,由 showPrompt*Panel 原子寫入,
186
+ * 避免只改 promptRightPanel 時 Suspense / 路由競態導致「生成報告設置」與按鈕錯位。
187
+ */
188
+ const uiShowPromptPanel = ref(true)
189
+ const uiShowGenerateSettingsPanel = ref(false)
190
+ const uiShowChapterEditToolbar = ref(true)
191
+ const uiShowFinalizeToolbar = ref(false)
192
+ const uiShowSettingsDoc = ref(false)
193
+ const uiShowEditorSummary = ref(true)
194
+
195
+ /**
196
+ * reportEdit 流程視圖(模組級,重掛不丟):與路由 step / rightPanel 對齊
197
+ */
198
+ const flowStep = ref<ReportModelStep>('select')
199
+ const flowRightPanel = ref<'prompt' | 'settings'>('prompt')
200
+
201
+ function applyPromptEditorUiFlags() {
202
+ promptRightPanel.value = 'prompt'
203
+ flowRightPanel.value = 'prompt'
204
+ uiShowPromptPanel.value = true
205
+ uiShowGenerateSettingsPanel.value = false
206
+ uiShowChapterEditToolbar.value = true
207
+ uiShowFinalizeToolbar.value = false
208
+ uiShowSettingsDoc.value = false
209
+ uiShowEditorSummary.value = true
210
+ }
211
+
212
+ function applyGenerateSettingsUiFlags() {
213
+ promptRightPanel.value = 'settings'
214
+ flowRightPanel.value = 'settings'
215
+ uiShowPromptPanel.value = false
216
+ uiShowGenerateSettingsPanel.value = true
217
+ uiShowChapterEditToolbar.value = false
218
+ uiShowFinalizeToolbar.value = true
219
+ uiShowSettingsDoc.value = true
220
+ uiShowEditorSummary.value = false
221
+ }
222
+
223
+ const generationDone = ref(false)
224
+ const generatedChapterBodies = ref<string[]>([])
225
+ /** 生成接口返回的章節標題(進入編輯後以接口為準) */
226
+ const generatedChapterTitles = ref<string[]>([])
227
+
228
+ /** 生成報告設置:章節勾選 / 排序(與中間主欄多章預覽同步) */
229
+ export type SettingsChapterPreset = { key: string; label: string }
230
+ export type SettingsChapterVersionOption = {
231
+ text: string
232
+ value: string
233
+ selected: boolean
234
+ uploadUsrNameDisplay?: string
235
+ }
236
+
237
+ /** AdjustInstance / 資訊源 selected 兼容 boolean、Y/N、1/0 */
238
+ function isGenAiSelectedFlag(flag: unknown): boolean {
239
+ if (flag === true || flag === 1) return true
240
+ if (flag === false || flag === 0 || flag == null) return false
241
+ const raw = String(flag).trim().toUpperCase()
242
+ return raw === 'Y' || raw === 'TRUE' || raw === '1'
243
+ }
244
+
245
+ const settingsChapterPresets = ref<SettingsChapterPreset[]>([])
246
+ const settingsSelectedChapters = ref<Record<string, boolean>>({})
247
+ const settingsChapterOrder = ref<string[]>([])
248
+ /** 右側章節版本下拉選項(AdjustInstance.instanceSections.versions) */
249
+ const settingsChapterVersionOptions = ref<Record<string, SettingsChapterVersionOption[]>>({})
250
+ /** 右側章節當前選中版本 value(UUID) */
251
+ const settingsChapterVersions = ref<Record<string, string>>({})
252
+ /** 右側章節生成人(AdjustInstance.instanceSections.createUserName) */
253
+ const settingsChapterGenerators = ref<Record<string, string>>({})
254
+ /**
255
+ * AdjustInstance / SimTradTransfer 返回的報告正文(報告頁中欄)
256
+ * 與右側「生成報告設置」語言下拉共享
257
+ */
258
+ const adjustInstanceContents = ref<Array<{ sectionTitle: string; sectionContent: string }>>([])
259
+
260
+ function setAdjustInstanceContents(
261
+ contents: Array<{
262
+ sectionTitle: string
263
+ sectionContent: string
264
+ refList?: unknown
265
+ Ref?: unknown
266
+ ref?: unknown
267
+ refs?: unknown
268
+ }>
269
+ ) {
270
+ const list = Array.isArray(contents) ? contents : []
271
+ // 靜態預覽(無打字機)也要先備好 refList,才能把 [ref:uuid] 解析成 tip
272
+ upsertChartStreamRefsFromSources(list)
273
+ adjustInstanceContents.value = list.map((item) => ({
274
+ sectionTitle: String(item.sectionTitle ?? ''),
275
+ sectionContent: String(item.sectionContent ?? ''),
276
+ }))
277
+ }
278
+
279
+ function upsertChapterChartStreamRefsFromSources(sources: unknown) {
280
+ return upsertChartStreamRefsFromSources(sources)
281
+ }
282
+
283
+ /**
284
+ * AdjustInstance / AdjustInstanceSetting 返回的已存在報告版本號(如 V1)
285
+ * 與報告歷史 instanceVersionNo 相等時,對應 card 為選中態(取第一個匹配)
286
+ */
287
+ const existsInstanceVersionNo = ref<string | null>(null)
288
+
289
+ function setExistsInstanceVersionNo(versionNo: unknown) {
290
+ if (versionNo == null || versionNo === '') {
291
+ existsInstanceVersionNo.value = null
292
+ return
293
+ }
294
+ existsInstanceVersionNo.value = String(versionNo).trim()
295
+ }
296
+
297
+ /**
298
+ * 連貫性檢查 CoherenceCheck.beenFinalizedFlag=Y 後:
299
+ * 工具列顯示「查看報告連貫性檢查結果」+「導出(Word)」
300
+ */
301
+ const coherenceResultReady = ref(false)
302
+ const coherenceCheckContentText = ref('')
303
+
304
+ function setCoherenceCheckResultReady(ready: boolean, contentText = '') {
305
+ coherenceResultReady.value = ready
306
+ coherenceCheckContentText.value = ready ? String(contentText ?? '') : ''
307
+ }
308
+
309
+ function clearCoherenceCheckResultReady() {
310
+ coherenceResultReady.value = false
311
+ coherenceCheckContentText.value = ''
312
+ }
313
+
314
+ /**
315
+ * 避免 AdjustInstance / AdjustInstanceSetting 回寫章節後
316
+ * 再次觸發「章節變更 → AdjustInstanceSetting → 打字機重播」死循環閃爍
317
+ */
318
+ const adjustInstanceSettingSyncKey = ref('')
319
+
320
+ function markAdjustInstanceSettingSynced(versionIdList: string[]) {
321
+ adjustInstanceSettingSyncKey.value = (versionIdList || [])
322
+ .map((id) => String(id ?? '').trim())
323
+ .filter(Boolean)
324
+ .join(',')
325
+ }
326
+
327
+ function isAdjustInstanceSettingSynced(versionIdList: string[]) {
328
+ const key = (versionIdList || [])
329
+ .map((id) => String(id ?? '').trim())
330
+ .filter(Boolean)
331
+ .join(',')
332
+ return !!key && key === adjustInstanceSettingSyncKey.value
333
+ }
334
+
335
+ /** 進頁 / 接口回寫章節時抑制 Settings 的 AdjustInstanceSetting watch */
336
+ const suppressAdjustInstanceSettingWatch = ref(false)
337
+
338
+ function beginSuppressAdjustInstanceSettingWatch() {
339
+ suppressAdjustInstanceSettingWatch.value = true
340
+ }
341
+
342
+ function endSuppressAdjustInstanceSettingWatch() {
343
+ void nextTick(() => {
344
+ suppressAdjustInstanceSettingWatch.value = false
345
+ })
346
+ }
347
+ /** 拖拽排序後:通知中欄從該章節起重新打字機列印(key + token 確保可重複觸發) */
348
+ const settingsTypewriterReplayKey = ref<string | null>(null)
349
+ const settingsTypewriterReplayToken = ref(0)
350
+ /** 中欄打字機當前章(供右側列表等鬆耦合滾動,不反向依賴中欄 DOM) */
351
+ const settingsTypingChapterKey = ref<string | null>(null)
352
+ /** 中欄是否正在打字機列印(列印中隱藏滾動條) */
353
+ const settingsTypewriterPrinting = ref(false)
354
+
355
+ function requestSettingsTypewriterReplay(key: string) {
356
+ if (!key) return
357
+ settingsTypewriterReplayKey.value = key
358
+ settingsTypewriterReplayToken.value += 1
359
+ }
360
+
361
+ function setSettingsTypingChapterKey(key: string | null) {
362
+ settingsTypingChapterKey.value = key
363
+ }
364
+
365
+ function setSettingsTypewriterPrinting(printing: boolean) {
366
+ settingsTypewriterPrinting.value = printing
367
+ }
368
+
369
+ /** 需求 2026-08-13 16:58:進入編輯 / settings 不打字;點「生成章節內容」才打字 */
370
+ const chapterTypewriterActive = ref(false)
371
+ /** 生成章節:等待首包;中欄 v-loading,首包到達後關閉 */
372
+ const chapterContentAwaitingFirstChunk = ref(false)
373
+
374
+ function startChapterContentTypewriter() {
375
+ chapterHistoryPreviewBody.value = ''
376
+ chapterTypewriterActive.value = true
377
+ chapterContentAwaitingFirstChunk.value = true
378
+ chapterHistoryApplyToken.value += 1
379
+ }
380
+
381
+ function markChapterContentFirstChunk() {
382
+ chapterContentAwaitingFirstChunk.value = false
383
+ }
384
+
385
+ /** 停止章節打字機;cancelStream 用於切章時中止 chartAsStream */
386
+ const chapterContentStreamEpoch = ref(0)
387
+
388
+ function stopChapterContentTypewriter(options?: { cancelStream?: boolean }) {
389
+ chapterTypewriterActive.value = false
390
+ chapterContentAwaitingFirstChunk.value = false
391
+ if (options?.cancelStream) chapterContentStreamEpoch.value += 1
392
+ }
393
+
394
+ const CHAPTER_CHART_AUDIT_EVENT_STORAGE_KEY = 'gen-ai-chapter-chart-audit-event-id'
395
+ const chapterChartAuditEventId = ref('')
396
+
397
+ function setChapterChartAuditEventId(id: string) {
398
+ const next = String(id ?? '').trim()
399
+ chapterChartAuditEventId.value = next
400
+ try {
401
+ if (next) sessionStorage.setItem(CHAPTER_CHART_AUDIT_EVENT_STORAGE_KEY, next)
402
+ else sessionStorage.removeItem(CHAPTER_CHART_AUDIT_EVENT_STORAGE_KEY)
403
+ } catch {
404
+ /* ignore */
405
+ }
406
+ }
407
+
408
+ function clearChapterChartAuditEventId() {
409
+ chapterChartAuditEventId.value = ''
410
+ try {
411
+ sessionStorage.removeItem(CHAPTER_CHART_AUDIT_EVENT_STORAGE_KEY)
412
+ } catch {
413
+ /* ignore */
414
+ }
415
+ }
416
+
417
+ function upsertChapterChartStreamRef(raw: unknown) {
418
+ return upsertChartStreamRef(raw)
419
+ }
420
+
421
+ function clearChapterChartStreamRefs() {
422
+ clearChartStreamRefs()
423
+ }
424
+
425
+ /** 保存時取值:內存 → sessionStorage 兜底(始終返回 string) */
426
+ function resolveChapterChartAuditEventId() {
427
+ const fromMem = String(chapterChartAuditEventId.value ?? '').trim()
428
+ if (fromMem) return fromMem
429
+ try {
430
+ return String(sessionStorage.getItem(CHAPTER_CHART_AUDIT_EVENT_STORAGE_KEY) ?? '').trim()
431
+ } catch {
432
+ return ''
433
+ }
434
+ }
435
+
436
+ function patchGeneratedChapterBody(index: number, body: string) {
437
+ if (index < 0) return
438
+ const next = [...generatedChapterBodies.value]
439
+ while (next.length <= index) next.push('')
440
+ next[index] = body
441
+ generatedChapterBodies.value = next
442
+ saveGeneratedBodiesToSession()
443
+ }
444
+
445
+ function clearSettingsTypewriterReplay() {
446
+ settingsTypewriterReplayKey.value = null
447
+ }
448
+ const sourceFiles = ref<SourceFile[]>([])
449
+ const pendingFiles = ref<PendingFile[]>([])
450
+ const reportEditors = ref<ReportEditor[]>([])
451
+ const reportHistoryItems = ref<ReportHistoryItem[]>([])
452
+ const activeHistoryKey = ref('')
453
+ const historyScrollTargetKey = ref<string | null>(null)
454
+ const historyPreviewRecordId = ref<string | null>(null)
455
+ const historyPreviewLayoutBaseline = ref<string | null>(null)
456
+ const historyPreviewDirty = ref(false)
457
+ const historyPreviewApplyToken = ref(0)
458
+ const historyPreviewChapterBodies = ref<string[]>([])
459
+ const chapterHistoryItems = ref<ReportHistoryItem[]>([])
460
+ const activeChapterHistoryKey = ref('')
461
+ const chapterHistoryPreviewBody = ref<string | null>(null)
462
+ const chapterHistoryApplyToken = ref(0)
463
+ /** 章節歷史折疊面板:無內容默認收起;出現「未生成草稿」時展開 */
464
+ const chapterHistoryOpen = ref(false)
465
+ const promptChapterForm = reactive<PromptChapterForm>(createDefaultPromptChapterForm())
466
+ const chapterEditMode = ref(false)
467
+ let chapterEditSnapshot: string | null = null
468
+
469
+ const aiPromptDialogRequestToken = ref(0)
470
+ const aiPromptDialogSource = ref<AiPromptDialogSource>('prompt-panel')
471
+
472
+ function requestOpenAiPromptDialog(source: AiPromptDialogSource = 'prompt-panel') {
473
+ aiPromptDialogSource.value = source
474
+ aiPromptDialogRequestToken.value += 1
475
+ }
476
+
477
+ /** 設置頁「生成報告」chartAsStream 緩衝(接口調用寫在 Settings 組件) */
478
+ const reportGenerateStreamText = ref('')
479
+ const reportGenerateStreamActive = ref(false)
480
+ const reportGenerateStreamEnded = ref(false)
481
+
482
+ function beginReportGenerateStream() {
483
+ reportGenerateStreamText.value = ''
484
+ reportGenerateStreamActive.value = true
485
+ reportGenerateStreamEnded.value = false
486
+ }
487
+
488
+ function appendReportGenerateStream(chunk: string) {
489
+ reportGenerateStreamText.value += chunk
490
+ }
491
+
492
+ function finishReportGenerateStream() {
493
+ reportGenerateStreamEnded.value = true
494
+ }
495
+
496
+ function clearReportGenerateStream() {
497
+ reportGenerateStreamText.value = ''
498
+ reportGenerateStreamActive.value = false
499
+ reportGenerateStreamEnded.value = false
500
+ }
501
+
502
+ function serializeChapterEditState() {
503
+ return JSON.stringify({
504
+ prompt: { ...promptChapterForm },
505
+ sources: sourceFiles.value.map((file) => ({ id: file.id, selected: file.selected })),
506
+ })
507
+ }
508
+
509
+ function restoreChapterEditSnapshot(snapshot: string) {
510
+ const parsed = JSON.parse(snapshot) as {
511
+ prompt: PromptChapterForm
512
+ sources: Array<{ id: string; selected: boolean }>
513
+ }
514
+ Object.assign(promptChapterForm, parsed.prompt)
515
+ for (const file of sourceFiles.value) {
516
+ const saved = parsed.sources.find((item) => item.id === file.id)
517
+ if (saved) file.selected = saved.selected
518
+ }
519
+ }
520
+
521
+ function isChapterEditDirty() {
522
+ if (!chapterEditSnapshot) return false
523
+ return serializeChapterEditState() !== chapterEditSnapshot
524
+ }
525
+
526
+ function enterChapterEditMode() {
527
+ stopChapterContentTypewriter()
528
+ chapterEditSnapshot = serializeChapterEditState()
529
+ chapterEditMode.value = true
530
+ }
531
+
532
+ function commitChapterEdit() {
533
+ chapterEditSnapshot = serializeChapterEditState()
534
+ chapterEditMode.value = false
535
+ }
536
+
537
+ function exitChapterEditMode(revert = false) {
538
+ if (revert && chapterEditSnapshot) {
539
+ restoreChapterEditSnapshot(chapterEditSnapshot)
540
+ } else if (chapterEditMode.value) {
541
+ chapterEditSnapshot = serializeChapterEditState()
542
+ }
543
+ chapterEditMode.value = false
544
+ }
545
+
546
+ function resetChapterEditMode() {
547
+ chapterEditSnapshot = null
548
+ chapterEditMode.value = false
549
+ }
550
+
551
+ function saveDraftToSession() {
552
+ sessionStorage.setItem(STORAGE_KEY, JSON.stringify({ ...draft }))
553
+ }
554
+
555
+ /** 章節歷史首行插入 / 刷新「未生成草稿」卡片;出現時強制展開章節歷史 */
556
+ function upsertChapterDraftHistory() {
557
+ const draftItem: ReportHistoryItem = {
558
+ key: CHAPTER_DRAFT_HISTORY_KEY,
559
+ title: CHAPTER_DRAFT_HISTORY_TITLE,
560
+ author: '',
561
+ finalizedAt: new Date().toISOString(),
562
+ kind: 'draft',
563
+ }
564
+ const rest = chapterHistoryItems.value.filter((item) => item.kind !== 'draft')
565
+ chapterHistoryItems.value = [draftItem, ...rest]
566
+ activeChapterHistoryKey.value = draftItem.key
567
+ chapterHistoryOpen.value = true
568
+ }
569
+
570
+ function removeChapterDraftHistory() {
571
+ const index = chapterHistoryItems.value.findIndex((item) => item.kind === 'draft')
572
+ if (index === -1) return false
573
+ chapterHistoryItems.value.splice(index, 1)
574
+ if (activeChapterHistoryKey.value === CHAPTER_DRAFT_HISTORY_KEY) {
575
+ const nextKey = chapterHistoryItems.value[0]?.key ?? ''
576
+ activeChapterHistoryKey.value = nextKey
577
+ }
578
+ return true
579
+ }
580
+
581
+ function loadDraftFromSession() {
582
+ const raw = sessionStorage.getItem(STORAGE_KEY)
583
+ if (!raw) return
584
+ try {
585
+ const parsed = JSON.parse(raw) as Record<string, unknown>
586
+ const legacyContentLanguage = parsed.chapterContentLanguage
587
+ if (legacyContentLanguage === 'zh') {
588
+ parsed.chapterContentLanguage = parsed.language === 'zh-Hans' ? 'zh-Hans' : 'zh-Hant'
589
+ } else if (legacyContentLanguage === 'en') {
590
+ parsed.chapterContentLanguage = 'zh-Hant'
591
+ }
592
+ Object.assign(draft, parsed as Partial<ReportDraft>)
593
+ } catch {
594
+ // ignore invalid cache
595
+ }
596
+ }
597
+
598
+ function saveTemplateToSession() {
599
+ if (!selectedTemplate.value) {
600
+ sessionStorage.removeItem(TEMPLATE_STORAGE_KEY)
601
+ return
602
+ }
603
+ sessionStorage.setItem(TEMPLATE_STORAGE_KEY, JSON.stringify(selectedTemplate.value))
604
+ }
605
+
606
+ function loadTemplateFromSession() {
607
+ const raw = sessionStorage.getItem(TEMPLATE_STORAGE_KEY)
608
+ if (!raw) return
609
+ try {
610
+ selectedTemplate.value = JSON.parse(raw) as ReportTemplateCard
611
+ } catch {
612
+ sessionStorage.removeItem(TEMPLATE_STORAGE_KEY)
613
+ }
614
+ }
615
+
616
+ function saveSourcesToSession() {
617
+ sessionStorage.setItem(
618
+ SOURCES_STORAGE_KEY,
619
+ JSON.stringify(
620
+ sourceFiles.value.map((file) => ({ id: file.id, name: file.name, selected: file.selected }))
621
+ )
622
+ )
623
+ }
624
+
625
+ function loadSourcesFromSession() {
626
+ const raw = sessionStorage.getItem(SOURCES_STORAGE_KEY)
627
+ if (!raw) return
628
+ try {
629
+ const parsed = JSON.parse(raw) as SourceFile[]
630
+ if (!Array.isArray(parsed) || !parsed.length) return
631
+ sourceFiles.value = parsed.map((file) => ({
632
+ id: file.id,
633
+ name: file.name,
634
+ selected: !!file.selected,
635
+ }))
636
+ } catch {
637
+ sessionStorage.removeItem(SOURCES_STORAGE_KEY)
638
+ }
639
+ }
640
+
641
+ function saveEditorsToSession() {
642
+ sessionStorage.setItem(EDITORS_STORAGE_KEY, JSON.stringify(reportEditors.value))
643
+ }
644
+
645
+ function loadEditorsFromSession() {
646
+ const raw = sessionStorage.getItem(EDITORS_STORAGE_KEY)
647
+ if (!raw) return
648
+ try {
649
+ const parsed = JSON.parse(raw) as ReportEditor[]
650
+ if (!Array.isArray(parsed) || !parsed.length) return
651
+ // 過濾歷史本地假編輯者(陳大文等),避免兩頁殘留 mock
652
+ const cleaned = parsed
653
+ .map((editor) => ({
654
+ id: String(editor.id),
655
+ name: String(editor.name ?? ''),
656
+ employeeId: String(editor.employeeId ?? editor.id),
657
+ department: String(editor.department ?? ''),
658
+ role: (editor.role === 'initiator' ? 'initiator' : 'editor') as ReportEditorRole,
659
+ }))
660
+ .filter((editor) => {
661
+ const id = editor.employeeId || editor.id
662
+ if (['8000121', '8000122', '8000123', '8808808'].includes(id)) return false
663
+ if (
664
+ editor.name.includes('陳大文') ||
665
+ editor.name.includes('陳大大') ||
666
+ editor.name.includes('張小小')
667
+ )
668
+ return false
669
+ return true
670
+ })
671
+ reportEditors.value = cleaned
672
+ if (cleaned.length !== parsed.length) saveEditorsToSession()
673
+ } catch {
674
+ sessionStorage.removeItem(EDITORS_STORAGE_KEY)
675
+ }
676
+ }
677
+
678
+ function saveGeneratedBodiesToSession() {
679
+ sessionStorage.setItem(BODIES_STORAGE_KEY, JSON.stringify(generatedChapterBodies.value))
680
+ sessionStorage.setItem(TITLES_STORAGE_KEY, JSON.stringify(generatedChapterTitles.value))
681
+ }
682
+
683
+ function loadGeneratedBodiesFromSession() {
684
+ const raw = sessionStorage.getItem(BODIES_STORAGE_KEY)
685
+ if (raw) {
686
+ try {
687
+ const parsed = JSON.parse(raw) as string[]
688
+ if (Array.isArray(parsed)) {
689
+ //0917 過濾 tip Mock 殘留正文
690
+ generatedChapterBodies.value = parsed.map((body) =>
691
+ looksLikeTipChartStreamMock(body) ? '' : String(body ?? '')
692
+ )
693
+ if (parsed.some((body) => looksLikeTipChartStreamMock(body))) {
694
+ saveGeneratedBodiesToSession()
695
+ }
696
+ }
697
+ } catch {
698
+ sessionStorage.removeItem(BODIES_STORAGE_KEY)
699
+ }
700
+ }
701
+ const rawTitles = sessionStorage.getItem(TITLES_STORAGE_KEY)
702
+ if (!rawTitles) return
703
+ try {
704
+ const parsed = JSON.parse(rawTitles) as string[]
705
+ if (Array.isArray(parsed)) generatedChapterTitles.value = parsed.map(String)
706
+ } catch {
707
+ sessionStorage.removeItem(TITLES_STORAGE_KEY)
708
+ }
709
+ }
710
+
711
+ function saveSettingsToSession() {
712
+ sessionStorage.setItem(
713
+ SETTINGS_STORAGE_KEY,
714
+ JSON.stringify({
715
+ presets: settingsChapterPresets.value,
716
+ selected: settingsSelectedChapters.value,
717
+ order: settingsChapterOrder.value,
718
+ })
719
+ )
720
+ }
721
+
722
+ function loadSettingsFromSession() {
723
+ const raw = sessionStorage.getItem(SETTINGS_STORAGE_KEY)
724
+ if (!raw) return
725
+ try {
726
+ const parsed = JSON.parse(raw) as {
727
+ presets?: SettingsChapterPreset[]
728
+ selected?: Record<string, boolean>
729
+ order?: string[]
730
+ }
731
+ if (parsed.presets?.length) settingsChapterPresets.value = parsed.presets
732
+ if (parsed.selected) settingsSelectedChapters.value = parsed.selected
733
+ if (parsed.order?.length) settingsChapterOrder.value = parsed.order
734
+ } catch {
735
+ sessionStorage.removeItem(SETTINGS_STORAGE_KEY)
736
+ }
737
+ }
738
+
739
+ function savePromptFormToSession() {
740
+ sessionStorage.setItem(PROMPT_FORM_STORAGE_KEY, JSON.stringify({ ...promptChapterForm }))
741
+ }
742
+
743
+ function clearPromptTableDefaults() {
744
+ promptChapterForm.tableTitle = ''
745
+ promptChapterForm.tableRowLabels = ''
746
+ promptChapterForm.tableColumnLabels = ''
747
+ promptChapterForm.tableContent = ''
748
+ promptChapterForm.chartDescription = ''
749
+ }
750
+
751
+ function loadPromptFormFromSession() {
752
+ const raw = sessionStorage.getItem(PROMPT_FORM_STORAGE_KEY)
753
+ if (!raw) {
754
+ if (!sessionStorage.getItem(PROMPT_TABLE_DEFAULTS_CLEARED_KEY)) {
755
+ sessionStorage.setItem(PROMPT_TABLE_DEFAULTS_CLEARED_KEY, '1')
756
+ }
757
+ return
758
+ }
759
+ try {
760
+ const parsed = JSON.parse(raw) as Partial<PromptChapterForm>
761
+ Object.assign(promptChapterForm, createDefaultPromptChapterForm(), parsed)
762
+ // 舊 session 可能帶有表格預填範例,統一清空為默認空值
763
+ if (!sessionStorage.getItem(PROMPT_TABLE_DEFAULTS_CLEARED_KEY)) {
764
+ clearPromptTableDefaults()
765
+ sessionStorage.setItem(PROMPT_TABLE_DEFAULTS_CLEARED_KEY, '1')
766
+ savePromptFormToSession()
767
+ }
768
+ } catch {
769
+ sessionStorage.removeItem(PROMPT_FORM_STORAGE_KEY)
770
+ }
771
+ }
772
+
773
+ export type GeneratingSessionChapter = {
774
+ id: string
775
+ sectionId: string
776
+ sectionName: string
777
+ orderSeq: number
778
+ status: 'pending' | 'creating' | 'success' | 'failure'
779
+ body?: string
780
+ }
781
+
782
+ export type GeneratingSessionSnapshot = {
783
+ templateId: string
784
+ instanceId: string
785
+ chapters: GeneratingSessionChapter[]
786
+ isComplete: boolean
787
+ }
788
+
789
+ export function saveGeneratingSession(snapshot: GeneratingSessionSnapshot) {
790
+ sessionStorage.setItem(GENERATING_STORAGE_KEY, JSON.stringify(snapshot))
791
+ }
792
+
793
+ export function loadGeneratingSession(): GeneratingSessionSnapshot | null {
794
+ const raw = sessionStorage.getItem(GENERATING_STORAGE_KEY)
795
+ if (!raw) return null
796
+ try {
797
+ return JSON.parse(raw) as GeneratingSessionSnapshot
798
+ } catch {
799
+ sessionStorage.removeItem(GENERATING_STORAGE_KEY)
800
+ return null
801
+ }
802
+ }
803
+
804
+ export function clearGeneratingSession() {
805
+ sessionStorage.removeItem(GENERATING_STORAGE_KEY)
806
+ }
807
+
808
+ /** 瀏覽器刷新後從 session 恢復 build-report 編輯態 */
809
+ function restoreReportEditSession() {
810
+ //0917 清掉 tip 聯調 Mock 殘留(英文 Grade A / 中文甲級寫字樓 / CBRE tip)
811
+ purgeTipChartStreamMockResidue()
812
+ loadDraftFromSession()
813
+ loadTemplateFromSession()
814
+ loadSourcesFromSession()
815
+ loadEditorsFromSession()
816
+ loadGeneratedBodiesFromSession()
817
+ loadSettingsFromSession()
818
+ loadPromptFormFromSession()
819
+ }
820
+
821
+ /** //0917 tip 聯調 Mock 指紋:清 session 殘留,避免刪源碼後畫面仍顯示假正文 */
822
+ const TIP_CHART_STREAM_MOCK_RE =
823
+ /Grade A Offices\s*[—\-]\s*H1 2024|甲級寫字樓\s*2024\s*年上半年|POC source file\s*-\s*CBRE_2024/i
824
+
825
+ function looksLikeTipChartStreamMock(text: unknown) {
826
+ return TIP_CHART_STREAM_MOCK_RE.test(String(text ?? ''))
827
+ }
828
+
829
+ function purgeTipChartStreamMockResidue() {
830
+ try {
831
+ clearChartStreamRefs()
832
+ sessionStorage.removeItem('gen-ai-chart-stream-refs')
833
+ sessionStorage.removeItem(CHAPTER_CHART_AUDIT_EVENT_STORAGE_KEY)
834
+ const bodiesRaw = sessionStorage.getItem(BODIES_STORAGE_KEY)
835
+ if (bodiesRaw && looksLikeTipChartStreamMock(bodiesRaw)) {
836
+ sessionStorage.removeItem(BODIES_STORAGE_KEY)
837
+ sessionStorage.removeItem(TITLES_STORAGE_KEY)
838
+ }
839
+ const generatingRaw = sessionStorage.getItem(GENERATING_STORAGE_KEY)
840
+ if (generatingRaw && looksLikeTipChartStreamMock(generatingRaw)) {
841
+ sessionStorage.removeItem(GENERATING_STORAGE_KEY)
842
+ }
843
+ } catch {
844
+ /* ignore */
845
+ }
846
+ generatedChapterBodies.value = generatedChapterBodies.value.map((body) =>
847
+ looksLikeTipChartStreamMock(body) ? '' : body
848
+ )
849
+ if (looksLikeTipChartStreamMock(chapterHistoryPreviewBody.value)) {
850
+ chapterHistoryPreviewBody.value = null
851
+ }
852
+ if (looksLikeTipChartStreamMock(reportGenerateStreamText.value)) {
853
+ clearReportGenerateStream()
854
+ }
855
+ }
856
+
857
+ watch(
858
+ sourceFiles,
859
+ () => {
860
+ draft.sourceCount = sourceFiles.value.filter((file) => file.selected).length
861
+ saveDraftToSession()
862
+ saveSourcesToSession()
863
+ },
864
+ { deep: true }
865
+ )
866
+
867
+ watch(
868
+ reportEditors,
869
+ () => {
870
+ saveEditorsToSession()
871
+ },
872
+ { deep: true }
873
+ )
874
+
875
+ watch(
876
+ [settingsChapterPresets, settingsSelectedChapters, settingsChapterOrder],
877
+ () => {
878
+ if (!settingsChapterPresets.value.length) return
879
+ saveSettingsToSession()
880
+ },
881
+ { deep: true }
882
+ )
883
+
884
+ watch(
885
+ promptChapterForm,
886
+ () => {
887
+ savePromptFormToSession()
888
+ },
889
+ { deep: true }
890
+ )
891
+
892
+ export function useReportWorkflow() {
893
+ const hasTemplate = computed(() => !!selectedTemplate.value)
894
+
895
+ const currentUserEditor = computed(
896
+ () => reportEditors.value.find((editor) => editor.id === CURRENT_USER_ID) ?? null
897
+ )
898
+ const isReportInitiator = computed(() => currentUserEditor.value?.role === 'initiator')
899
+ const isReportEditor = computed(() => currentUserEditor.value?.role === 'editor')
900
+ const canManageReportEditors = computed(() => isReportInitiator.value)
901
+ const canEditReport = computed(() => isReportInitiator.value || isReportEditor.value)
902
+
903
+ function patchDraft(partial: Partial<ReportDraft>) {
904
+ Object.assign(draft, partial)
905
+ saveDraftToSession()
906
+ }
907
+
908
+ function setSelectedTemplate(template: ReportTemplateCard | null) {
909
+ selectedTemplate.value = template
910
+ saveTemplateToSession()
911
+ }
912
+
913
+ function setModelStep(step: ReportModelStep) {
914
+ modelStep.value = step
915
+ flowStep.value = step
916
+ if (step !== 'prompt') {
917
+ applyPromptEditorUiFlags()
918
+ resetChapterEditMode()
919
+ }
920
+ }
921
+
922
+ function beginReportEditNavigation(step: ReportModelStep, rightPanel?: PromptRightPanel) {
923
+ reportEditNavLocked.value = true
924
+ reportEditNavTarget.value = step
925
+ reportEditNavRightPanel.value =
926
+ rightPanel ?? (step === 'prompt' ? promptRightPanel.value : null)
927
+ editViewOpen.value = true
928
+ // 先寫模組級 flowStep,再 remount 也不會丟(一鍵生成點一次即可)
929
+ setModelStep(step)
930
+ if (step === 'prompt') {
931
+ if (rightPanel === 'settings') applyGenerateSettingsUiFlags()
932
+ else if (rightPanel === 'prompt') applyPromptEditorUiFlags()
933
+ } else {
934
+ flowRightPanel.value = 'prompt'
935
+ }
936
+ }
937
+
938
+ function endReportEditNavigation() {
939
+ const target = reportEditNavTarget.value
940
+ const panel = reportEditNavRightPanel.value
941
+ if (target) {
942
+ setModelStep(target)
943
+ }
944
+ if (target === 'prompt') {
945
+ if (panel === 'settings') applyGenerateSettingsUiFlags()
946
+ else applyPromptEditorUiFlags()
947
+ } else if (target) {
948
+ flowRightPanel.value = 'prompt'
949
+ }
950
+ reportEditNavTarget.value = null
951
+ reportEditNavRightPanel.value = null
952
+ reportEditNavLocked.value = false
953
+ }
954
+
955
+ function isReportEditNavLocked() {
956
+ return reportEditNavLocked.value
957
+ }
958
+
959
+ function remountReportEditPage() {
960
+ reportEditMountKey.value += 1
961
+ }
962
+
963
+ function ensureSettingsChapterLayout(chapters?: string[]) {
964
+ // 僅接受接口章節標題;無數據則空列表(不再回落模板 / 硬編碼 mock)
965
+ const source = chapters?.length ? chapters : []
966
+ if (!source.length) {
967
+ settingsChapterPresets.value = []
968
+ settingsChapterOrder.value = []
969
+ settingsSelectedChapters.value = {}
970
+ settingsChapterVersionOptions.value = {}
971
+ settingsChapterVersions.value = {}
972
+ settingsChapterGenerators.value = {}
973
+ return
974
+ }
975
+ const unavailable = new Set(['附件與數'])
976
+ const presets = source.map((title, index) => ({
977
+ key: `chapter-${index}`,
978
+ label: stripChapterNumberPrefix(title),
979
+ }))
980
+ settingsChapterPresets.value = presets
981
+ settingsChapterOrder.value = presets.map((preset) => preset.key)
982
+ settingsSelectedChapters.value = Object.fromEntries(
983
+ presets.map((preset) => [preset.key, !unavailable.has(preset.label)])
984
+ )
985
+ settingsChapterVersionOptions.value = {}
986
+ settingsChapterVersions.value = Object.fromEntries(presets.map((preset) => [preset.key, '']))
987
+ saveSettingsToSession()
988
+ }
989
+
990
+ /**
991
+ * AdjustInstance.instanceSections → 右側章節列表 + 版本下拉
992
+ * - sectionTitle → 章節名稱
993
+ * - versions[].text → 版本 select 下拉展示
994
+ * - versions[].value → select 綁定值(UUID)
995
+ * - selected → 章節是否勾選(優先 section.selected;否則 versions 任一 selected)
996
+ * - 版本下拉默認項:versions 中 selected 為真的項,否則取第一個
997
+ */
998
+ function applyAdjustInstanceSections(
999
+ sections: Array<{
1000
+ sectionTitle: string
1001
+ selected?: boolean | string | number
1002
+ createUserName?: string
1003
+ versions?: Array<{
1004
+ text: string
1005
+ value: string
1006
+ selected?: boolean | string | number
1007
+ uploadUsrNameDisplay?: string
1008
+ }>
1009
+ }>
1010
+ ) {
1011
+ const list = Array.isArray(sections) ? sections : []
1012
+ const presets = list.map((section, index) => ({
1013
+ key: `chapter-${index}`,
1014
+ label: stripChapterNumberPrefix(section.sectionTitle || `章節${index + 1}`),
1015
+ }))
1016
+ settingsChapterPresets.value = presets
1017
+ settingsChapterOrder.value = presets.map((preset) => preset.key)
1018
+
1019
+ const optionsMap: Record<string, SettingsChapterVersionOption[]> = {}
1020
+ const versionValueMap: Record<string, string> = {}
1021
+ const checkedMap: Record<string, boolean> = {}
1022
+ const generatorMap: Record<string, string> = {}
1023
+
1024
+ list.forEach((section, index) => {
1025
+ const key = `chapter-${index}`
1026
+ const sectionGenerator = String(section.createUserName ?? '').trim()
1027
+ const versions = (Array.isArray(section.versions) ? section.versions : []).map((item) => ({
1028
+ text: String(item.text ?? ''),
1029
+ value: String(item.value ?? ''),
1030
+ selected: isGenAiSelectedFlag(item.selected),
1031
+ uploadUsrNameDisplay: String(
1032
+ item.uploadUsrNameDisplay ?? sectionGenerator ?? ''
1033
+ ).trim(),
1034
+ }))
1035
+ optionsMap[key] = versions
1036
+
1037
+ // 版本下拉默認:selected 的版本,否則第一個(展示 text,如 V3)
1038
+ const picked = versions.find((item) => item.selected) ?? versions[0]
1039
+ versionValueMap[key] = picked?.value ?? ''
1040
+
1041
+ // 章節勾選:優先章節自身 selected;否則看 versions 是否有 selected
1042
+ if (section.selected !== undefined && section.selected !== null && section.selected !== '') {
1043
+ checkedMap[key] = isGenAiSelectedFlag(section.selected)
1044
+ } else {
1045
+ checkedMap[key] = versions.some((item) => item.selected)
1046
+ }
1047
+
1048
+ generatorMap[key] = sectionGenerator || String(picked?.uploadUsrNameDisplay ?? '').trim()
1049
+ })
1050
+
1051
+ settingsChapterVersionOptions.value = optionsMap
1052
+ settingsChapterVersions.value = versionValueMap
1053
+ settingsSelectedChapters.value = checkedMap
1054
+ settingsChapterGenerators.value = generatorMap
1055
+ saveSettingsToSession()
1056
+ }
1057
+
1058
+ /**
1059
+ * AdjustInstance / AdjustInstanceSetting.instanceFileList → 資訊源列表
1060
+ * - text → 檔案名稱
1061
+ * - value → 文件 UUID(id)
1062
+ * - selected → 勾選反顯(報告歷史環節只讀,不可主動改)
1063
+ */
1064
+ function applyAdjustInstanceFiles(
1065
+ files: Array<{ text: string; value: string; selected?: boolean | string | number }>
1066
+ ) {
1067
+ const list = Array.isArray(files) ? files : []
1068
+ sourceFiles.value = list.map((file) => ({
1069
+ id: String(file.value ?? ''),
1070
+ name: String(file.text ?? ''),
1071
+ selected: isGenAiSelectedFlag(file.selected),
1072
+ }))
1073
+ }
1074
+
1075
+ function clearAdjustInstanceBindings() {
1076
+ settingsChapterPresets.value = []
1077
+ settingsChapterOrder.value = []
1078
+ settingsSelectedChapters.value = {}
1079
+ settingsChapterVersionOptions.value = {}
1080
+ settingsChapterVersions.value = {}
1081
+ settingsChapterGenerators.value = {}
1082
+ adjustInstanceContents.value = []
1083
+ existsInstanceVersionNo.value = null
1084
+ clearCoherenceCheckResultReady()
1085
+ adjustInstanceSettingSyncKey.value = ''
1086
+ // 不清空 sourceFiles:返回章節編輯後「生成報告」仍依賴本地資訊源校驗;
1087
+ // 進設置頁時由 AdjustInstance → applyAdjustInstanceFiles 覆蓋即可。
1088
+ }
1089
+
1090
+ function showPromptSettingsPanel() {
1091
+ exitChapterEditMode(true)
1092
+ // 章節佈局等 AdjustInstance;不再用模板 mock 預填
1093
+ applyGenerateSettingsUiFlags()
1094
+ }
1095
+
1096
+ function showPromptEditorPanel() {
1097
+ applyPromptEditorUiFlags()
1098
+ }
1099
+
1100
+ function openReportEdit(step: ReportModelStep = 'select') {
1101
+ // 鎖定期只允許跳到導航目標,避免中間態路由回寫
1102
+ if (
1103
+ reportEditNavLocked.value &&
1104
+ reportEditNavTarget.value &&
1105
+ step !== reportEditNavTarget.value
1106
+ ) {
1107
+ return
1108
+ }
1109
+ editViewOpen.value = true
1110
+ setModelStep(step)
1111
+ // 右側欄(prompt / settings)由 showPrompt*Panel 單獨控制,此處不覆寫
1112
+ }
1113
+
1114
+ function closeReportEdit() {
1115
+ if (reportEditNavLocked.value) return
1116
+ editViewOpen.value = false
1117
+ }
1118
+
1119
+ function setGeneratedChapterBodies(bodies: string[]) {
1120
+ generatedChapterBodies.value = bodies
1121
+ saveGeneratedBodiesToSession()
1122
+ }
1123
+
1124
+ /** 進入編輯:以生成接口章節列表為準寫入標題 + 正文 */
1125
+ function setGeneratedChaptersFromApi(chapters: Array<{ title: string; body: string }>) {
1126
+ generatedChapterTitles.value = chapters.map((item) => item.title)
1127
+ generatedChapterBodies.value = chapters.map((item) => item.body)
1128
+ saveGeneratedBodiesToSession()
1129
+ if (generatedChapterTitles.value.length) {
1130
+ ensureSettingsChapterLayout(generatedChapterTitles.value)
1131
+ }
1132
+ }
1133
+
1134
+ /**
1135
+ * 每次進入 ReportChapterWorkspace:清除歷史預覽 / 章節歷史 / Prompt,
1136
+ * 待 getSectionVersionList 等接口覆蓋(不回填 mock)
1137
+ */
1138
+ function resetWorkspaceHistoryOnEnter() {
1139
+ resetHistoryPreviewState()
1140
+ historyPreviewApplyToken.value += 1
1141
+ chapterHistoryItems.value = []
1142
+ activeChapterHistoryKey.value = ''
1143
+ chapterHistoryPreviewBody.value = null
1144
+ chapterHistoryApplyToken.value += 1
1145
+ Object.assign(promptChapterForm, createDefaultPromptChapterForm())
1146
+ resetChapterEditMode()
1147
+ }
1148
+
1149
+ function markGenerationSuccess() {
1150
+ generationDone.value = true
1151
+ editViewOpen.value = true
1152
+ // 不在此處改 modelStep:由 handleEnterEditing → navigateAndRemount('prompt') 統一切頁
1153
+ // 避免「store 已是 prompt、路由仍是 generating」導致畫面與路由互打
1154
+ clearGeneratingSession()
1155
+ }
1156
+
1157
+ function resetWorkflow() {
1158
+ selectedTemplate.value = null
1159
+ modelStep.value = 'select'
1160
+ generationDone.value = false
1161
+ applyPromptEditorUiFlags()
1162
+ }
1163
+
1164
+ function prependReportHistory(title: string, recordId: string, finalizedAt?: string) {
1165
+ const key = `history-${Date.now()}`
1166
+ const author =
1167
+ reportEditors.value.find((editor) => editor.role === 'initiator')?.name?.trim() ||
1168
+ reportEditors.value.find((editor) => editor.id === CURRENT_USER_ID)?.name?.trim() ||
1169
+ ''
1170
+ reportHistoryItems.value.unshift({
1171
+ key,
1172
+ title,
1173
+ author,
1174
+ finalizedAt: finalizedAt ?? new Date().toISOString(),
1175
+ recordId,
1176
+ })
1177
+ activeHistoryKey.value = key
1178
+ historyScrollTargetKey.value = key
1179
+ return key
1180
+ }
1181
+
1182
+ function clearHistoryScrollTarget() {
1183
+ historyScrollTargetKey.value = null
1184
+ }
1185
+
1186
+ function applyHistoryPreviewSources(sourceFileIds: string[]) {
1187
+ const selectedSet = new Set(sourceFileIds)
1188
+ for (const file of sourceFiles.value) {
1189
+ file.selected = selectedSet.has(file.id)
1190
+ }
1191
+ }
1192
+
1193
+ function selectHistoryReport(recordId: string) {
1194
+ const record = findFinalizedReportById(recordId)
1195
+ if (!record) return
1196
+
1197
+ const historyItem = reportHistoryItems.value.find((item) => item.recordId === recordId)
1198
+ if (historyItem) {
1199
+ activeHistoryKey.value = historyItem.key
1200
+ }
1201
+
1202
+ historyPreviewRecordId.value = recordId
1203
+ historyPreviewLayoutBaseline.value = serializeSelectedChapterLabels(record.chapters)
1204
+ historyPreviewDirty.value = false
1205
+ historyPreviewChapterBodies.value = buildReportHistoryChapterBodies(record)
1206
+ historyPreviewApplyToken.value += 1
1207
+ chapterHistoryPreviewBody.value = null
1208
+ chapterHistoryApplyToken.value += 1
1209
+
1210
+ applyHistoryPreviewSources(record.sourceFileIds)
1211
+ patchDraft({
1212
+ language: record.reportLanguage,
1213
+ chapterContentLanguage: record.chapterContentLanguage,
1214
+ })
1215
+ showPromptSettingsPanel()
1216
+ }
1217
+
1218
+ function markHistoryPreviewDirty() {
1219
+ if (!historyPreviewRecordId.value || historyPreviewDirty.value) return
1220
+ historyPreviewDirty.value = true
1221
+ historyPreviewRecordId.value = null
1222
+ historyPreviewLayoutBaseline.value = null
1223
+ historyPreviewChapterBodies.value = []
1224
+ }
1225
+
1226
+ function resetHistoryPreviewState() {
1227
+ historyPreviewRecordId.value = null
1228
+ historyPreviewLayoutBaseline.value = null
1229
+ historyPreviewDirty.value = false
1230
+ historyPreviewChapterBodies.value = []
1231
+ }
1232
+
1233
+ function selectChapterHistory(key: string) {
1234
+ const item = chapterHistoryItems.value.find((entry) => entry.key === key)
1235
+ if (!item) return false
1236
+
1237
+ activeChapterHistoryKey.value = key
1238
+ // 正文 / Prompt 由 ReportChapterWorkspace 依接口版本列表 watch 套用;不再回落本地 mock 快照
1239
+ return true
1240
+ }
1241
+
1242
+ function renameChapterHistory(key: string, title: string) {
1243
+ const trimmed = title.trim()
1244
+ if (!trimmed) return false
1245
+
1246
+ const item = chapterHistoryItems.value.find((entry) => entry.key === key)
1247
+ if (!item) return false
1248
+
1249
+ item.title = trimmed.slice(0, 50)
1250
+ return true
1251
+ }
1252
+
1253
+ function deleteChapterHistory(key: string) {
1254
+ const index = chapterHistoryItems.value.findIndex((entry) => entry.key === key)
1255
+ if (index === -1) return false
1256
+
1257
+ chapterHistoryItems.value.splice(index, 1)
1258
+
1259
+ if (activeChapterHistoryKey.value === key) {
1260
+ const nextKey = chapterHistoryItems.value[0]?.key ?? ''
1261
+ activeChapterHistoryKey.value = nextKey
1262
+ if (!nextKey) {
1263
+ chapterHistoryPreviewBody.value = null
1264
+ chapterHistoryApplyToken.value += 1
1265
+ }
1266
+ }
1267
+
1268
+ return true
1269
+ }
1270
+
1271
+ return {
1272
+ draft,
1273
+ selectedTemplate,
1274
+ modelStep,
1275
+ editViewOpen,
1276
+ reportEditMountKey,
1277
+ remountReportEditPage,
1278
+ beginReportEditNavigation,
1279
+ endReportEditNavigation,
1280
+ isReportEditNavLocked,
1281
+ promptRightPanel,
1282
+ flowStep,
1283
+ flowRightPanel,
1284
+ uiShowPromptPanel,
1285
+ uiShowGenerateSettingsPanel,
1286
+ uiShowChapterEditToolbar,
1287
+ uiShowFinalizeToolbar,
1288
+ uiShowSettingsDoc,
1289
+ uiShowEditorSummary,
1290
+ generationDone,
1291
+ generatedChapterBodies,
1292
+ generatedChapterTitles,
1293
+ settingsChapterPresets,
1294
+ settingsSelectedChapters,
1295
+ settingsChapterOrder,
1296
+ settingsChapterVersionOptions,
1297
+ settingsChapterVersions,
1298
+ settingsChapterGenerators,
1299
+ adjustInstanceContents,
1300
+ setAdjustInstanceContents,
1301
+ existsInstanceVersionNo,
1302
+ setExistsInstanceVersionNo,
1303
+ coherenceResultReady,
1304
+ coherenceCheckContentText,
1305
+ setCoherenceCheckResultReady,
1306
+ clearCoherenceCheckResultReady,
1307
+ adjustInstanceSettingSyncKey,
1308
+ markAdjustInstanceSettingSynced,
1309
+ isAdjustInstanceSettingSynced,
1310
+ suppressAdjustInstanceSettingWatch,
1311
+ beginSuppressAdjustInstanceSettingWatch,
1312
+ endSuppressAdjustInstanceSettingWatch,
1313
+ applyAdjustInstanceSections,
1314
+ applyAdjustInstanceFiles,
1315
+ clearAdjustInstanceBindings,
1316
+ settingsTypewriterReplayKey,
1317
+ settingsTypewriterReplayToken,
1318
+ settingsTypingChapterKey,
1319
+ settingsTypewriterPrinting,
1320
+ requestSettingsTypewriterReplay,
1321
+ setSettingsTypingChapterKey,
1322
+ setSettingsTypewriterPrinting,
1323
+ chapterTypewriterActive,
1324
+ chapterContentAwaitingFirstChunk,
1325
+ chapterContentStreamEpoch,
1326
+ startChapterContentTypewriter,
1327
+ markChapterContentFirstChunk,
1328
+ stopChapterContentTypewriter,
1329
+ chapterChartAuditEventId,
1330
+ setChapterChartAuditEventId,
1331
+ clearChapterChartAuditEventId,
1332
+ resolveChapterChartAuditEventId,
1333
+ upsertChapterChartStreamRef,
1334
+ clearChapterChartStreamRefs,
1335
+ upsertChapterChartStreamRefsFromSources,
1336
+ reportGenerateStreamText,
1337
+ reportGenerateStreamActive,
1338
+ reportGenerateStreamEnded,
1339
+ beginReportGenerateStream,
1340
+ appendReportGenerateStream,
1341
+ finishReportGenerateStream,
1342
+ clearReportGenerateStream,
1343
+ patchGeneratedChapterBody,
1344
+ clearSettingsTypewriterReplay,
1345
+ sourceFiles,
1346
+ pendingFiles,
1347
+ reportEditors,
1348
+ reportHistoryItems,
1349
+ activeHistoryKey,
1350
+ historyScrollTargetKey,
1351
+ historyPreviewRecordId,
1352
+ historyPreviewLayoutBaseline,
1353
+ historyPreviewDirty,
1354
+ historyPreviewApplyToken,
1355
+ historyPreviewChapterBodies,
1356
+ chapterHistoryItems,
1357
+ activeChapterHistoryKey,
1358
+ chapterHistoryPreviewBody,
1359
+ chapterHistoryApplyToken,
1360
+ chapterHistoryOpen,
1361
+ prependReportHistory,
1362
+ clearHistoryScrollTarget,
1363
+ selectHistoryReport,
1364
+ markHistoryPreviewDirty,
1365
+ resetHistoryPreviewState,
1366
+ selectChapterHistory,
1367
+ renameChapterHistory,
1368
+ deleteChapterHistory,
1369
+ currentUserEditor,
1370
+ isReportInitiator,
1371
+ isReportEditor,
1372
+ canManageReportEditors,
1373
+ canEditReport,
1374
+ hasTemplate,
1375
+ promptChapterForm,
1376
+ chapterEditMode,
1377
+ aiPromptDialogRequestToken,
1378
+ aiPromptDialogSource,
1379
+ requestOpenAiPromptDialog,
1380
+ enterChapterEditMode,
1381
+ commitChapterEdit,
1382
+ exitChapterEditMode,
1383
+ isChapterEditDirty,
1384
+ resetChapterEditMode,
1385
+ saveDraftToSession,
1386
+ upsertChapterDraftHistory,
1387
+ removeChapterDraftHistory,
1388
+ loadDraftFromSession,
1389
+ loadTemplateFromSession,
1390
+ restoreReportEditSession,
1391
+ saveGeneratingSession,
1392
+ loadGeneratingSession,
1393
+ clearGeneratingSession,
1394
+ patchDraft,
1395
+ setSelectedTemplate,
1396
+ setModelStep,
1397
+ showPromptSettingsPanel,
1398
+ ensureSettingsChapterLayout,
1399
+ showPromptEditorPanel,
1400
+ openReportEdit,
1401
+ closeReportEdit,
1402
+ setGeneratedChapterBodies,
1403
+ setGeneratedChaptersFromApi,
1404
+ resetWorkspaceHistoryOnEnter,
1405
+ markGenerationSuccess,
1406
+ resetWorkflow,
1407
+ }
1408
+ }