aegon-lv97 1.0.0

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