aegon-gen 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 (86) hide show
  1. package/package.json +12 -0
  2. package/src/App.vue +3 -0
  3. package/src/api/index.ts +19 -0
  4. package/src/api/modules/gen-ai/gen-entry/index.ts +30 -0
  5. package/src/api/modules/gen-ai/model-manager/index.ts +42 -0
  6. package/src/api/modules/gen-ai/model-manager/mockApi.ts +33 -0
  7. package/src/api/modules/index.ts +98 -0
  8. package/src/api/modules/user/index.ts +4 -0
  9. package/src/api/request.ts +102 -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 +93 -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 +142 -0
  21. package/src/components/GEN-AI/index1.vue +456 -0
  22. package/src/components/GEN-AI/index10.vue +5 -0
  23. package/src/components/GEN-AI/index2.vue +568 -0
  24. package/src/components/GEN-AI/index3.vue +623 -0
  25. package/src/components/GEN-AI/index4.vue +629 -0
  26. package/src/components/GEN-AI/index5.vue +578 -0
  27. package/src/components/GEN-AI/index6.vue +656 -0
  28. package/src/components/GEN-AI/index7.vue +717 -0
  29. package/src/components/GEN-AI/index8.vue +405 -0
  30. package/src/components/GEN-AI/index9.vue +1065 -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 +7 -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/components/el-date-picker.vue +64 -0
  42. package/src/directives/btnLoading.ts +427 -0
  43. package/src/directives/debounce copy.ts +670 -0
  44. package/src/directives/debounce.ts +98 -0
  45. package/src/directives/index.ts +25 -0
  46. package/src/layouts/MainLayout.vue +101 -0
  47. package/src/main.ts +85 -0
  48. package/src/router/index.ts +76 -0
  49. package/src/router/menus.ts +28 -0
  50. package/src/style.css +79 -0
  51. package/src/styles/_variables.scss +24 -0
  52. package/src/styles/app-button.css +26 -0
  53. package/src/styles/element-overrides.css +23 -0
  54. package/src/styles/global.css +44 -0
  55. package/src/styles/index.scss +1 -0
  56. package/src/styles/page-card.css +21 -0
  57. package/src/styles/variables.css +26 -0
  58. package/src/test/mock.ts +101 -0
  59. package/src/test/test1.vue +402 -0
  60. package/src/test/test2.vue +1689 -0
  61. package/src/types/gen-ai/gen-entry/index.ts +17 -0
  62. package/src/types/gen-ai/model-manager/index.ts +19 -0
  63. package/src/utils/docxExport.ts +1610 -0
  64. package/src/utils/gen-ai-navigation.ts +37 -0
  65. package/src/utils/gen-ai-scroll.ts +455 -0
  66. package/src/utils/openDataLoaderWordExport.ts +33 -0
  67. package/src/utils/pageScrollbar.ts +115 -0
  68. package/src/utils/randomTranscode.ts +87 -0
  69. package/src/utils/reportPdfExport.ts +44 -0
  70. package/src/views/AdminCenter/index.vue +817 -0
  71. package/src/views/Blank.vue +68 -0
  72. package/src/views/Home.vue +29 -0
  73. package/src/views/ReportCenter/index.vue +1380 -0
  74. package/src/views/TemplateCenter/Knowledge.ts +83 -0
  75. package/src/views/TemplateCenter/data.d.ts +10 -0
  76. package/src/views/TemplateCenter/index.vue +1205 -0
  77. package/src/views/TemplateCenter/service.ts +69 -0
  78. package/src/views/gen-ai/components/RecentReportsTable.vue +193 -0
  79. package/src/views/gen-ai/gen-entry/index.vue +309 -0
  80. package/src/views/gen-ai/gen-entry/mockData.ts +160 -0
  81. package/src/views/gen-ai/management-center/index.vue +53 -0
  82. package/src/views/gen-ai/model-manager/ChapterTitleScroll.vue +275 -0
  83. package/src/views/gen-ai/model-manager/index.vue +1205 -0
  84. package/src/views/gen-ai/model-manager/mockData.ts +122 -0
  85. package/src/views/gen-ai/report-center/index.vue +158 -0
  86. package/src/vite-env.d.ts +38 -0
@@ -0,0 +1,1380 @@
1
+ <!--
2
+ template-center 单文件版(含 PageHeader、API/SSE、Knowledge、章节对话)
3
+ 路由:{ path: '/template-center', component: () => import('@/test/test2.vue') }
4
+ 环境变量:VITE_REPORT_STREAM_URL、VITE_REPORT_STREAM_MOCK、VITE_CHAT_MODEL
5
+ -->
6
+ <template>
7
+ <div class="template-center-page">
8
+ <header class="page-header">
9
+ <h1 class="page-header__title">IR-DEMO</h1>
10
+ </header>
11
+
12
+ <div class="chatbot-report-shell">
13
+ <!-- 左侧:报告配置 -->
14
+ <aside class="chatbot-report-panel chatbot-report-panel--config left">
15
+ <div class="chatbot-report-panel__body">
16
+ <h3 class="chatbot-report-config-title">報告配置</h3>
17
+ <div class="chatbot-report-field">
18
+ <span class="chatbot-report-field__label">名稱</span>
19
+ <span class="chatbot-report-field__value">
20
+ {{ activeConversation?.label || 'Hedera 鏈上運營 2026' }}
21
+ </span>
22
+ <EditPen class="chatbot-report-field__edit" @click="newChat" />
23
+ </div>
24
+ <div class="chatbot-report-field">
25
+ <span class="chatbot-report-field__label">模型</span>
26
+ <span class="chatbot-report-field__value">
27
+ {{ useLocalFallback ? '本地知識庫' : displayModel }}
28
+ </span>
29
+ <EditPen class="chatbot-report-field__edit" @click="useLocalFallback = !useLocalFallback" />
30
+ </div>
31
+ <div class="chatbot-report-field chatbot-report-field--readonly">
32
+ <span class="chatbot-report-field__label">語言</span>
33
+ <span class="chatbot-report-field__value">中文</span>
34
+ </div>
35
+ <div class="chatbot-report-field chatbot-report-field--readonly">
36
+ <span class="chatbot-report-field__label">行業</span>
37
+ <span class="chatbot-report-field__value">Hedera 鏈上運營</span>
38
+ </div>
39
+ <div class="chatbot-report-field chatbot-report-field--readonly">
40
+ <span class="chatbot-report-field__label">年份</span>
41
+ <span class="chatbot-report-field__value">2026</span>
42
+ </div>
43
+
44
+ <div class="chatbot-report-section-title">信息源</div>
45
+ <div class="chatbot-report-add-source" role="presentation">
46
+ <div style="display: flex; align-items: center;justify-content: center;">
47
+ <span class="chatbot-report-add-source__icon">
48
+ <Plus />
49
+ </span>
50
+ </div>
51
+ <div>
52
+ 添加信息源
53
+ </div>
54
+ </div>
55
+ <button type="button" class="chatbot-report-web-search" @click="webSearch = !webSearch">
56
+ <CheckMark :checked="webSearch" />
57
+ 在網絡中搜索信息源
58
+ </button>
59
+
60
+ <div class="chatbot-report-source-select">
61
+ <div class="chatbot-report-select-all" @click="toggleAllDocs">
62
+ <span>選擇所有信息源</span>
63
+ <CheckMark :checked="allDocsSelected" />
64
+ </div>
65
+
66
+ <div class="chatbot-report-doc-group">
67
+ <div class="chatbot-report-doc-group__header chatbot-report-source-row"
68
+ @click="toggleDocGroup(SHARED_DOCS.map(d => d.id))">
69
+ <span class="chatbot-report-source-row__label">共享文檔集</span>
70
+ <CheckMark :checked="SHARED_DOCS.every(d => selectedDocs[d.id])" />
71
+ </div>
72
+ <div v-for="doc in SHARED_DOCS" :key="doc.id"
73
+ class="chatbot-report-source-row chatbot-report-source-row--doc" @click="toggleDoc(doc.id)">
74
+ <PdfIcon />
75
+ <span class="chatbot-report-source-row__label">{{ doc.title }}</span>
76
+ <CheckMark :checked="!!selectedDocs[doc.id]" />
77
+ </div>
78
+ </div>
79
+
80
+ <div class="chatbot-report-doc-group">
81
+ <div class="chatbot-report-doc-group__header chatbot-report-source-row"
82
+ @click="toggleDocGroup(PERSONAL_DOCS.map(d => d.id))">
83
+ <span class="chatbot-report-source-row__label">個人文檔集</span>
84
+ <CheckMark :checked="PERSONAL_DOCS.every(d => selectedDocs[d.id])" />
85
+ </div>
86
+ <div v-for="doc in PERSONAL_DOCS" :key="doc.id"
87
+ class="chatbot-report-source-row chatbot-report-source-row--doc" @click="toggleDoc(doc.id)">
88
+ <PdfIcon />
89
+ <span class="chatbot-report-source-row__label">{{ doc.title }}</span>
90
+ <CheckMark :checked="!!selectedDocs[doc.id]" />
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </aside>
96
+
97
+ <!-- 中间:对话预览(当前注释,保留占位) -->
98
+ <main class="chatbot-report-center center">
99
+ <div class="chatbot-report-toolbar">
100
+ <el-button class="chatbot-report-btn-outline" @click="backToChapterEdit">返回章节编辑</el-button>
101
+ <div>
102
+ <el-button class="chatbot-report-btn-outline" :icon="Download" :disabled="!reportPreviewVisible"
103
+ @click="exportReportPdf">
104
+ 匯出為 PDF
105
+ </el-button>
106
+ <el-button class="chatbot-report-btn-outline" :icon="Download" :disabled="!reportPreviewVisible"
107
+ @click="exportReportWord">
108
+ 匯出為 Word
109
+ </el-button>
110
+ </div>
111
+ </div>
112
+
113
+ <div v-if="!reportPreviewVisible" class="chatbot-report-center__empty">
114
+ <p>選擇章節後點擊「生成報告」預覽內容</p>
115
+ </div>
116
+ <div v-else class="chatbot-report-center__preview">
117
+ <div class="chatbot-report-stream-debug">
118
+ <div v-if="isRequesting" class="chatbot-report-stream-debug__status">输出中...</div>
119
+ <div class="chatbot-report-stream-debug__body">{{ reportStreamText || '等待 SSE 数据...' }}</div>
120
+ </div>
121
+ </div>
122
+ </main>
123
+
124
+ <!-- 右侧:生成報告設置 -->
125
+ <aside class="chatbot-report-panel chatbot-report-panel--settings right">
126
+ <div class="chatbot-report-panel__body">
127
+ <h3 class="chatbot-report-config-title">生成報告設置</h3>
128
+
129
+ <div class="chatbot-report-chapter-select">
130
+ <div class="chatbot-report-chapter-header">
131
+ <span class="chatbot-report-chapter-header__title">選擇章節及版本</span>
132
+ <div class="chatbot-report-chapter-header__all" @click="toggleAllChapters">
133
+ <CheckMark :checked="allChaptersSelected" />
134
+ <span>全部章節</span>
135
+ </div>
136
+ </div>
137
+ <div class="chatbot-report-chapter-list">
138
+ <div v-for="preset in orderedPresets" :key="preset.key" class="chatbot-report-chapter-row" :class="{
139
+ 'is-dragging': dragChapterKey === preset.key,
140
+ 'is-drag-over': dragOverChapterKey === preset.key,
141
+ }" draggable="true" @dragstart="onChapterDragStart($event, preset.key)" @dragend="handleChapterDragEnd"
142
+ @dragover.prevent="setDragOverChapterKey(preset.key)" @drop="handleChapterDrop(preset.key)">
143
+ <CheckMark :checked="!!selectedChapters[preset.key]" @click.stop="toggleChapter(preset.key)"
144
+ @mousedown.stop @dragstart.prevent />
145
+ <span class="chatbot-report-chapter-row__label"
146
+ @click="setActiveKey(preset.key); sendMessage(preset.label)">
147
+ {{ preset.label }}
148
+ </span>
149
+ <select class="chatbot-report-chapter-row__version" :value="chapterVersions[preset.key]"
150
+ @change="chapterVersions[preset.key] = ($event.target as HTMLSelectElement).value" @click.stop
151
+ @mousedown.stop>
152
+ <option v-for="v in CHAPTER_VERSIONS" :key="v" :value="v">{{ v }}</option>
153
+ </select>
154
+ </div>
155
+ </div>
156
+ </div>
157
+
158
+ <div class="chatbot-report-section-title chatbot-report-section-title--settings">生成報告方式</div>
159
+ <el-radio-group v-model="reportMode" class="chatbot-report-radio-group">
160
+ <el-radio value="polish">拼裝並潤色</el-radio>
161
+ <el-radio value="rewrite">重寫整篇報告</el-radio>
162
+ </el-radio-group>
163
+
164
+ <el-button class="chatbot-report-generate-btn" :disabled="isRequesting" v-debounce.lock.loading
165
+ @click="generateFromChapters">
166
+ 生成X報告
167
+ </el-button>
168
+
169
+ <div class="chatbot-report-history">
170
+ <div class="chatbot-report-history__title">報告歷史</div>
171
+ <div class="chatbot-report-history-list">
172
+ <div v-for="item in reportHistory" :key="item.key" class="chatbot-report-history-row"
173
+ :class="{ 'is-active': item.key === activeHistoryKey }" @click="activeHistoryKey = item.key">
174
+ <span class="chatbot-report-history-item__indicator" aria-hidden="true" />
175
+ <div class="chatbot-report-history-item">
176
+ <div class="chatbot-report-history-item__body">
177
+ <div class="chatbot-report-history-item__title">{{ item.title }}</div>
178
+ <div class="chatbot-report-history-item__meta">
179
+ {{ item.author }} {{ item.time }}
180
+ </div>
181
+ </div>
182
+ <MoreFilled class="chatbot-report-history-item__menu" @click.stop />
183
+ </div>
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ </aside>
189
+ </div>
190
+ </div>
191
+ </template>
192
+
193
+ <script setup lang="ts">
194
+ import { Download, EditPen, MoreFilled, Plus } from '@element-plus/icons-vue'
195
+ import { ElButton, ElRadio, ElRadioGroup } from 'element-plus'
196
+ import { computed, defineComponent, h, nextTick, onUnmounted, ref } from 'vue'
197
+
198
+ // —— API(原 api/request.ts + api/modules/report.ts)——
199
+ function getToken(): string {
200
+ return localStorage.getItem('token') || ''
201
+ }
202
+
203
+ function buildAuthHeaders(): HeadersInit {
204
+ const token = getToken()
205
+ return {
206
+ 'Content-Type': 'application/json',
207
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
208
+ }
209
+ }
210
+
211
+ interface PostRequestOptions {
212
+ url: string
213
+ data?: unknown
214
+ signal?: AbortSignal
215
+ }
216
+
217
+ interface SseBlock {
218
+ event: string
219
+ data: string
220
+ }
221
+
222
+ function parseSseBlock(block: string): SseBlock | null {
223
+ if (!block.trim()) return null
224
+ let event = ''
225
+ let data = ''
226
+ for (const line of block.split('\n')) {
227
+ if (line.startsWith('event:')) {
228
+ event = line.replace('event:', '').trim()
229
+ } else if (line.startsWith('data:')) {
230
+ data += line.replace('data:', '')
231
+ }
232
+ }
233
+ return { event, data }
234
+ }
235
+
236
+ async function postStreamRequest(options: PostRequestOptions): Promise<Response> {
237
+ const response = await fetch(options.url, {
238
+ method: 'POST',
239
+ headers: buildAuthHeaders(),
240
+ body: JSON.stringify(options.data ?? {}),
241
+ signal: options.signal,
242
+ })
243
+ if (!response.ok) {
244
+ throw new Error(`请求失败 ${response.status}`)
245
+ }
246
+ return response
247
+ }
248
+
249
+ async function* streamPostSse(options: PostRequestOptions): AsyncGenerator<SseBlock> {
250
+ const response = await postStreamRequest(options)
251
+ const reader = response.body?.getReader()
252
+ if (!reader) throw new Error('No reader')
253
+ const decoder = new TextDecoder('utf-8')
254
+ let buffer = ''
255
+ try {
256
+ while (true) {
257
+ const { done, value } = await reader.read()
258
+ if (done) break
259
+ buffer += decoder.decode(value, { stream: true })
260
+ const blocks = buffer.split('\n\n')
261
+ buffer = blocks.pop() || ''
262
+ for (const block of blocks) {
263
+ const parsed = parseSseBlock(block)
264
+ if (parsed) yield parsed
265
+ }
266
+ }
267
+ } finally {
268
+ reader.releaseLock()
269
+ }
270
+ }
271
+
272
+ const REPORT_STREAM_USE_MOCK = import.meta.env.VITE_REPORT_STREAM_MOCK === 'true'
273
+ const REPORT_STREAM_API_URL = import.meta.env.VITE_REPORT_STREAM_URL || '/sse/aiStream'
274
+
275
+ const MOCK_REPORT_STREAM_TEXT = `# 行业分析报告(Mock)
276
+
277
+ ## 摘要與本年總結
278
+
279
+ 本报告为本地 Mock 流式输出。接入后端 SSE 后由接口返回替换。
280
+
281
+ ## 宏觀環境與政策背景
282
+
283
+ 宏观政策延续稳健基调,行业监管框架逐步完善。`
284
+
285
+ interface GenerateReportParams {
286
+ userId?: string
287
+ question?: string
288
+ chapters?: string[]
289
+ chapterVersions?: Record<string, string>
290
+ reportMode?: string
291
+ }
292
+
293
+ function buildMockStreamText(params: GenerateReportParams): string {
294
+ const chapters = params.chapters?.length
295
+ ? params.chapters.map((label, i) => `## ${label}\n\n(Mock)第 ${i + 1} 章内容占位。\n`).join('\n')
296
+ : MOCK_REPORT_STREAM_TEXT
297
+ return `# 生成报告预览(Mock)\n\n**模式**:${params.reportMode === 'rewrite' ? '重寫整篇報告' : '拼裝並潤色'}\n\n${chapters}`
298
+ }
299
+
300
+ async function* streamMockReport(text: string): AsyncGenerator<string> {
301
+ for (const char of text) {
302
+ yield char
303
+ await new Promise((resolve) => setTimeout(resolve, 12))
304
+ }
305
+ }
306
+
307
+ async function* streamAiReportSse(
308
+ params: { userId: string; question: string },
309
+ signal?: AbortSignal,
310
+ ): AsyncGenerator<string> {
311
+ for await (const block of streamPostSse({
312
+ url: REPORT_STREAM_API_URL,
313
+ data: params,
314
+ signal,
315
+ })) {
316
+ if (block.event === 'text') yield block.data
317
+ if (block.event === 'end') return
318
+ }
319
+ }
320
+
321
+ async function* streamGenerateReport(
322
+ params: GenerateReportParams,
323
+ signal?: AbortSignal,
324
+ ): AsyncGenerator<string> {
325
+ if (REPORT_STREAM_USE_MOCK) {
326
+ yield* streamMockReport(buildMockStreamText(params))
327
+ return
328
+ }
329
+ yield* streamAiReportSse(
330
+ {
331
+ userId: params.userId ?? 'user001',
332
+ question: params.question ?? '介绍一下SSE流式传输',
333
+ },
334
+ signal,
335
+ )
336
+ }
337
+
338
+ const reportApi = { getToken }
339
+
340
+ // —— Knowledge(原 TemplateCenter/Knowledge.ts)——
341
+ const HEDERA_SYSTEM_PROMPT = `你是 HederaRoot 平台的 Hedera 链上运营助手。
342
+ 你熟悉 HTS(代币)、HCS(共识存证)、HSCS(智能合约)、Governance(KYC/密钥矩阵)、RWA 上链与 Mirror Node 监控。
343
+ 回答应简洁、可执行,优先给出页面路径(如 /hts/tokens、/governance/kyc)与 .env 配置提示。
344
+ 若涉及私钥,提醒用户仅在 .env 本地配置,不要提交到 Git。`
345
+
346
+ type KnowledgeEntry = { keywords: string[]; answer: string }
347
+
348
+ const KNOWLEDGE_BASE: KnowledgeEntry[] = [
349
+ { keywords: ['创建', '代币', 'hts', 'create'], answer: '创建 FT/NFT:打开 **/hts/create-token**,在 .env 配置 ACCOUNT_ID 与 PRIVATE_KEY。' },
350
+ { keywords: ['kyc', '白名单', '准入'], answer: 'KYC 流程:创建代币时设置 **KYC Key**;在 **/governance/kyc** 链上授权。' },
351
+ { keywords: ['暂停', 'pause', '熔断'], answer: '代币暂停需 **Pause Key**,在 **/hts/TokenServe** 执行暂停/恢复。' },
352
+ { keywords: ['rwa', 'metadata', '100'], answer: 'Hedera Token metadata 上限 **100 字节**,完整信息写入 **HCS Topic**。' },
353
+ { keywords: ['合约', 'hscs', 'deploy', '部署'], answer: '智能合约:**/hscs/contracts** 部署,**/hscs/debug** 调试。' },
354
+ { keywords: ['mirror', 'tps', '网络'], answer: '网络监控:**/settings/network** 与 **/dashboard/monitor**。' },
355
+ { keywords: ['hcs', '存证', 'topic'], answer: 'HCS 共识存证:**/hcs** 查看 Topic 消息流。' },
356
+ { keywords: ['env', 'account', '私钥', '配置'], answer: '在 `.env` 配置 ACCOUNT_ID、PRIVATE_KEY、HEDERA_RPC_URL。' },
357
+ ]
358
+
359
+ function matchLocalKnowledge(question: string): string | null {
360
+ const lower = question.toLowerCase()
361
+ let best: KnowledgeEntry | null = null
362
+ let bestScore = 0
363
+ for (const entry of KNOWLEDGE_BASE) {
364
+ let score = 0
365
+ for (const kw of entry.keywords) {
366
+ if (lower.includes(kw.toLowerCase())) score += 1
367
+ }
368
+ if (score > bestScore) {
369
+ bestScore = score
370
+ best = entry
371
+ }
372
+ }
373
+ return bestScore > 0 ? best!.answer : null
374
+ }
375
+
376
+ // —— Chat service(原 TemplateCenter/service.ts)——
377
+ const CHAT_API_URL = '/api/chat/completions'
378
+ const CHAT_MODEL = import.meta.env.VITE_CHAT_MODEL ?? 'qwen/qwen-2.5-7b-instruct:free'
379
+
380
+ async function* streamChatCompletion(messages: Array<{ role: string; content: string }>) {
381
+ const response = await fetch(CHAT_API_URL, {
382
+ method: 'POST',
383
+ headers: buildAuthHeaders(),
384
+ body: JSON.stringify({
385
+ model: CHAT_MODEL,
386
+ messages,
387
+ stream: true,
388
+ temperature: 0.7,
389
+ max_tokens: 2048,
390
+ }),
391
+ })
392
+ if (!response.ok) throw new Error(`HTTP ${response.status}`)
393
+ const reader = response.body?.getReader()
394
+ if (!reader) throw new Error('No reader')
395
+ const decoder = new TextDecoder()
396
+ let buffer = ''
397
+ while (true) {
398
+ const { done, value } = await reader.read()
399
+ if (done) break
400
+ buffer += decoder.decode(value, { stream: true })
401
+ const lines = buffer.split('\n')
402
+ buffer = lines.pop() || ''
403
+ for (const line of lines) {
404
+ const trimmed = line.trim()
405
+ if (!trimmed || trimmed === 'data: [DONE]') continue
406
+ if (!trimmed.startsWith('data: ')) continue
407
+ try {
408
+ const json = JSON.parse(trimmed.slice(6))
409
+ const delta = json.choices?.[0]?.delta?.content || ''
410
+ if (delta) yield delta
411
+ } catch {
412
+ /* ignore */
413
+ }
414
+ }
415
+ }
416
+ }
417
+
418
+ const displayModel = CHAT_MODEL.split('/').pop()?.replace(/:.*$/, '') ?? 'Qwen3-Max'
419
+
420
+ const SHARED_DOCS = [
421
+ { id: 'hts', title: 'Hedera HTS 代币创建与 KYC 操作指南' },
422
+ { id: 'hscs', title: 'HSCS 智能合约部署与调试手册' },
423
+ { id: 'rwa', title: 'RWA 上链 metadata 与 HCS 存证说明' },
424
+ ]
425
+ const PERSONAL_DOCS = [
426
+ { id: 'env', title: 'Operator .env 配置与 Mirror Node 说明' },
427
+ { id: 'gov', title: 'Governance 密钥矩阵与 KYC 准入流程' },
428
+ { id: 'audit', title: 'AI 审计与链上风险事件采样' },
429
+ ]
430
+ const ALL_DOC_IDS = [...SHARED_DOCS, ...PERSONAL_DOCS].map(d => d.id)
431
+ const HEDERA_PRESETS = [
432
+ { key: 'preset-summary', label: '摘要與本年總結', group: 'Hedera' },
433
+ { key: 'preset-macro', label: '宏觀環境與政策背景', group: 'Hedera' },
434
+ { key: 'preset-industry', label: '行業運行情況與核心指標', group: 'Hedera' },
435
+ { key: 'preset-competition', label: '競爭格局與商業模式演變', group: 'Hedera' },
436
+ { key: 'preset-risk', label: '關鍵風險與監管關切', group: 'Hedera' },
437
+ { key: 'preset-appendix', label: '附錄:數據、方法與名詞解釋', group: 'Hedera' },
438
+ ]
439
+ const CHAPTER_VERSIONS = ['V1', 'V2', 'V3']
440
+
441
+ type ParsedMessage = { role: 'user'; content: string } | { role: 'assistant'; content: string; thinkContent?: string }
442
+ type ConversationItem = { key: string; label: string; group?: string; isDraft?: boolean }
443
+ type ReportHistoryItem = { key: string; title: string; author: string; time: string }
444
+
445
+ const CheckMark = defineComponent({
446
+ name: 'CheckMark',
447
+ inheritAttrs: false,
448
+ props: {
449
+ checked: { type: Boolean, required: true },
450
+ },
451
+ emits: ['click'],
452
+ setup(props, { emit, attrs }) {
453
+ return () =>
454
+ h('span', {
455
+ ...attrs,
456
+ class: ['chatbot-report-check', props.checked && 'is-checked', attrs.class],
457
+ onClick: (e: MouseEvent) => emit('click', e),
458
+ })
459
+ },
460
+ })
461
+
462
+ const PdfIcon = defineComponent({
463
+ name: 'PdfIcon',
464
+ setup() {
465
+ return () =>
466
+ h(
467
+ 'svg',
468
+ {
469
+ xmlns: 'http://www.w3.org/2000/svg',
470
+ viewBox: '0 0 1024 1024',
471
+ class: 'chatbot-report-doc-card__pdf',
472
+ },
473
+ [
474
+ h('path', {
475
+ fill: 'currentColor',
476
+ d: 'M832 384H576V128H192v768h640zm-26.496-64L640 154.496V320zM160 64h480l256 256v608a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32m160 448h384v64H320zm0-192h160v64H320zm0 384h384v64H320z',
477
+ }),
478
+ ],
479
+ )
480
+ },
481
+ })
482
+
483
+ const useLocalFallback = ref(false)
484
+ const webSearch = ref(true)
485
+ const conversations = ref<ConversationItem[]>([
486
+ { key: 'default', label: 'Hedera 运营问答', group: '今天', isDraft: true },
487
+ ...HEDERA_PRESETS,
488
+ ])
489
+ const activeKey = ref('default')
490
+ const activeHistoryKey = ref('history-final')
491
+ const reportHistory = ref<ReportHistoryItem[]>([
492
+ { key: 'history-final', title: '定稿版本', author: '陳大文', time: '15min前' },
493
+ { key: 'history-v2', title: 'V2', author: '陳大文', time: '15min前' },
494
+ ])
495
+ const selectedDocs = ref<Record<string, boolean>>(Object.fromEntries(ALL_DOC_IDS.map(id => [id, true])))
496
+ const selectedChapters = ref<Record<string, boolean>>(Object.fromEntries(HEDERA_PRESETS.map(p => [p.key, true])))
497
+ const chapterVersions = ref<Record<string, string>>(Object.fromEntries(HEDERA_PRESETS.map(p => [p.key, 'V3'])))
498
+ const chapterOrder = ref<string[]>(HEDERA_PRESETS.map(p => p.key))
499
+ const reportMode = ref('polish')
500
+ const isRequesting = ref(false)
501
+ const reportPreviewVisible = ref(false)
502
+ const reportStreamText = ref('')
503
+ let reportStreamAbort: AbortController | null = null
504
+ const messages = ref<ParsedMessage[]>([])
505
+ const dragChapterKey = ref<string | null>(null)
506
+ const dragOverChapterKey = ref<string | null>(null)
507
+
508
+ const setDragOverChapterKey = (key: string) => {
509
+ dragOverChapterKey.value = key
510
+ }
511
+
512
+ const activeConversation = computed(() => conversations.value.find(c => c.key === activeKey.value))
513
+ const orderedPresets = computed(() => chapterOrder.value.map(key => HEDERA_PRESETS.find(p => p.key === key)!).filter(Boolean))
514
+ const allDocsSelected = computed(() => ALL_DOC_IDS.every(id => selectedDocs.value[id]))
515
+ const allChaptersSelected = computed(() => HEDERA_PRESETS.every(p => selectedChapters.value[p.key]))
516
+
517
+ const toggleDoc = (id: string) => {
518
+ selectedDocs.value[id] = !selectedDocs.value[id]
519
+ }
520
+ const toggleAllDocs = () => {
521
+ const next = !allDocsSelected.value
522
+ selectedDocs.value = Object.fromEntries(ALL_DOC_IDS.map(id => [id, next]))
523
+ }
524
+ const toggleDocGroup = (ids: string[]) => {
525
+ const allOn = ids.every(id => selectedDocs.value[id])
526
+ ids.forEach(id => {
527
+ selectedDocs.value[id] = !allOn
528
+ })
529
+ }
530
+ const toggleChapter = (key: string) => {
531
+ selectedChapters.value[key] = !selectedChapters.value[key]
532
+ }
533
+ const toggleAllChapters = () => {
534
+ const next = !allChaptersSelected.value
535
+ HEDERA_PRESETS.forEach(p => {
536
+ selectedChapters.value[p.key] = next
537
+ })
538
+ }
539
+ const setActiveKey = (key: string) => {
540
+ activeKey.value = key
541
+ }
542
+ const newChat = () => {
543
+ const key = crypto.randomUUID()
544
+ conversations.value = [{ key, label: '新对话', group: '今天', isDraft: true }, ...conversations.value.filter(c => c.key !== key)]
545
+ activeKey.value = key
546
+ messages.value = []
547
+ }
548
+ const backToChapterEdit = () => {
549
+ reportPreviewVisible.value = false
550
+ }
551
+ const generateFromChapters = async () => {
552
+ const selected = orderedPresets.value.filter(p => selectedChapters.value[p.key])
553
+ if (!selected.length) return
554
+
555
+ const token = reportApi.getToken()
556
+ if (!token) {
557
+ alert('请先登录')
558
+ return
559
+ }
560
+
561
+ if (reportStreamAbort) reportStreamAbort.abort()
562
+ reportStreamAbort = new AbortController()
563
+
564
+ isRequesting.value = true
565
+ reportStreamText.value = ''
566
+ reportPreviewVisible.value = true
567
+
568
+ const question = selected.map(p => p.label).join('、')
569
+
570
+ try {
571
+ for await (const chunk of streamGenerateReport(
572
+ {
573
+ userId: 'user001',
574
+ question,
575
+ chapters: selected.map(p => p.label),
576
+ chapterVersions: Object.fromEntries(
577
+ selected.map((p) => [p.key, chapterVersions.value[p.key] ?? 'V3']),
578
+ ),
579
+ reportMode: reportMode.value,
580
+ },
581
+ reportStreamAbort.signal,
582
+ )) {
583
+ reportStreamText.value += chunk
584
+ }
585
+ } catch (err) {
586
+ if ((err as Error).name !== 'AbortError') {
587
+ console.error('流异常', err)
588
+ reportStreamText.value = `[SSE 请求失败] ${(err as Error).message}\n\n${MOCK_REPORT_STREAM_TEXT}`
589
+ }
590
+ } finally {
591
+ isRequesting.value = false
592
+ reportStreamAbort = null
593
+ }
594
+ }
595
+
596
+ onUnmounted(() => {
597
+ reportStreamAbort?.abort()
598
+ })
599
+ function downloadReportText(ext: 'txt' | 'md') {
600
+ if (!reportStreamText.value) return
601
+ const blob = new Blob([reportStreamText.value], { type: 'text/plain;charset=utf-8' })
602
+ const url = URL.createObjectURL(blob)
603
+ const a = document.createElement('a')
604
+ a.href = url
605
+ a.download = `report-${Date.now()}.${ext}`
606
+ a.click()
607
+ URL.revokeObjectURL(url)
608
+ }
609
+
610
+ const exportReportPdf = () => downloadReportText('txt')
611
+ const exportReportWord = () => downloadReportText('md')
612
+
613
+ const handleChapterDragStart = (key: string) => {
614
+ dragChapterKey.value = key
615
+ dragOverChapterKey.value = key
616
+ }
617
+ const onChapterDragStart = (event: DragEvent, key: string) => {
618
+ if ((event.target as HTMLElement).closest('.chatbot-report-check, .chatbot-report-chapter-row__version')) {
619
+ event.preventDefault()
620
+ return
621
+ }
622
+ handleChapterDragStart(key)
623
+ }
624
+ const handleChapterDragEnd = () => {
625
+ dragChapterKey.value = null
626
+ dragOverChapterKey.value = null
627
+ }
628
+ const handleChapterDrop = (targetKey: string) => {
629
+ const draggingKey = dragChapterKey.value
630
+ if (draggingKey && draggingKey !== targetKey) {
631
+ const newOrder = [...chapterOrder.value]
632
+ const fromIdx = newOrder.indexOf(draggingKey)
633
+ const toIdx = newOrder.indexOf(targetKey)
634
+ if (fromIdx !== -1 && toIdx !== -1) {
635
+ newOrder.splice(fromIdx, 1)
636
+ newOrder.splice(toIdx, 0, draggingKey)
637
+ chapterOrder.value = newOrder
638
+ }
639
+ }
640
+ handleChapterDragEnd()
641
+ }
642
+
643
+ const sendMessage = async (text: string) => {
644
+ if (!text.trim()) return
645
+ messages.value.push({ role: 'user', content: text })
646
+ if (useLocalFallback.value) {
647
+ const answer = matchLocalKnowledge(text) ?? '本地知识库暂未收录该问题。请切换「云端模型」或尝试:创建代币、KYC、RWA、HSCS、暂停代币、Mirror Node 等关键词。'
648
+ messages.value.push({ role: 'assistant', content: answer })
649
+ return
650
+ }
651
+ isRequesting.value = true
652
+ const apiMessages = [
653
+ { role: 'system', content: HEDERA_SYSTEM_PROMPT },
654
+ { role: 'user', content: text },
655
+ ]
656
+ try {
657
+ let fullContent = ''
658
+ messages.value.push({ role: 'assistant', content: '' })
659
+ const lastIndex = messages.value.length - 1
660
+ for await (const chunk of streamChatCompletion(apiMessages)) {
661
+ fullContent += chunk
662
+ messages.value[lastIndex] = { role: 'assistant', content: fullContent }
663
+ await nextTick()
664
+ }
665
+ } catch (err) {
666
+ console.error(err)
667
+ messages.value.push({ role: 'assistant', content: '请求失败,请检查网络或 API 配置。' })
668
+ } finally {
669
+ isRequesting.value = false
670
+ }
671
+ }
672
+ </script>
673
+
674
+ <style scoped>
675
+ .page-header {
676
+ flex-shrink: 0;
677
+ height: 62px;
678
+ display: flex;
679
+ align-items: center;
680
+ padding: 0 24px;
681
+ background: #fff;
682
+ border-bottom: 1px solid #e8eaec;
683
+ }
684
+
685
+ .page-header__title {
686
+ margin: 0;
687
+ font-size: 16px;
688
+ font-weight: 600;
689
+ color: #2d3139;
690
+ line-height: 1;
691
+ }
692
+
693
+ .template-center-page {
694
+ display: flex;
695
+ flex-direction: column;
696
+ height: 100vh;
697
+ overflow: hidden;
698
+ background: var(--report-bg, #f3f3f3);
699
+ }
700
+
701
+ .chatbot-report-shell {
702
+ --report-accent: #c53355;
703
+ --report-accent-soft: #fdf0f2;
704
+ --report-border: #ebebeb;
705
+ --report-bg: #f3f3f3;
706
+ --report-text: #1a1a1a;
707
+ --report-muted: #999;
708
+ --report-card-bg: #f5f5f5;
709
+ --report-add-blue: #4a90d9;
710
+
711
+ flex: 1;
712
+ display: flex;
713
+ gap: 0px;
714
+ min-height: 0;
715
+ min-width: 0;
716
+ overflow: hidden;
717
+ background: var(--report-bg);
718
+ padding: 20px;
719
+ }
720
+
721
+ .chatbot-report-shell .left,
722
+ .chatbot-report-shell .right {
723
+ width: 20%;
724
+ min-width: 280px;
725
+ flex-shrink: 0;
726
+ }
727
+
728
+ .chatbot-report-shell .center {
729
+ flex: 1;
730
+ min-width: 520px;
731
+ min-height: 0;
732
+ display: flex;
733
+ flex-direction: column;
734
+ }
735
+
736
+ .chatbot-report-panel {
737
+ background: #fff;
738
+ border: 0px solid var(--report-border);
739
+ border-radius: 2px;
740
+ display: flex;
741
+ flex-direction: column;
742
+ min-height: 0;
743
+ overflow: hidden;
744
+ }
745
+
746
+ .chatbot-report-panel--config .chatbot-report-panel__body,
747
+ .chatbot-report-panel--settings .chatbot-report-panel__body {
748
+ padding: 16px 18px 20px;
749
+ }
750
+
751
+ .chatbot-report-panel__head {
752
+ padding: 14px 16px 10px;
753
+ font-size: 15px;
754
+ font-weight: 600;
755
+ color: var(--report-text);
756
+ border-bottom: 1px solid #f0f0f0;
757
+ flex-shrink: 0;
758
+ }
759
+
760
+ .chatbot-report-panel__body {
761
+ flex: 1;
762
+ overflow-y: auto;
763
+ }
764
+
765
+ .chatbot-report-config-title {
766
+ font-size: 15px;
767
+ font-weight: 600;
768
+ color: var(--report-text);
769
+ margin: 0 0 4px;
770
+ }
771
+
772
+ .chatbot-report-field {
773
+ display: grid;
774
+ grid-template-columns: 72px 1fr auto;
775
+ align-items: center;
776
+ gap: 8px;
777
+ padding: 11px 0;
778
+ border-bottom: 1px solid #f0f0f0;
779
+ font-size: 13px;
780
+ }
781
+
782
+ .chatbot-report-field--readonly {
783
+ grid-template-columns: 72px 1fr;
784
+ }
785
+
786
+ .chatbot-report-field__label {
787
+ color: var(--report-muted);
788
+ }
789
+
790
+ .chatbot-report-field__value {
791
+ text-align: right;
792
+ color: #555;
793
+ overflow: hidden;
794
+ text-overflow: ellipsis;
795
+ white-space: nowrap;
796
+ }
797
+
798
+ .chatbot-report-field__edit {
799
+ color: var(--report-accent);
800
+ cursor: pointer;
801
+ font-size: 13px;
802
+ }
803
+
804
+ .chatbot-report-section-title {
805
+ margin: 18px 0 12px;
806
+ font-size: 14px;
807
+ font-weight: 600;
808
+ color: var(--report-text);
809
+ }
810
+
811
+ .chatbot-report-add-source {
812
+ border: 1px dashed #d9d9d9;
813
+ border-radius: 20px;
814
+ padding: 20px;
815
+ text-align: center;
816
+ color: var(--report-muted);
817
+ cursor: pointer;
818
+ font-size: 13px;
819
+ background: #fff;
820
+ transition: border-color 0.2s;
821
+ }
822
+
823
+ .chatbot-report-add-source:hover {
824
+ border-color: var(--report-add-blue);
825
+ }
826
+
827
+ .chatbot-report-add-source__icon {
828
+ display: block;
829
+ font-size: 24px;
830
+ line-height: 1;
831
+ margin-bottom: 8px;
832
+ color: var(--report-add-blue);
833
+ width: 20px;
834
+ }
835
+
836
+ .chatbot-report-doc-card__pdf {
837
+ width: 14px;
838
+ height: 14px;
839
+ color: #c53355;
840
+ flex-shrink: 0;
841
+ }
842
+
843
+ .chatbot-report-web-search {
844
+ display: flex;
845
+ align-items: center;
846
+ gap: 10px;
847
+ width: 100%;
848
+ padding: 10px 14px;
849
+ border: none;
850
+ border-radius: 6px;
851
+ background: var(--report-card-bg);
852
+ font-size: 13px;
853
+ color: #555;
854
+ cursor: pointer;
855
+ text-align: left;
856
+ transition: background 0.15s;
857
+ box-sizing: border-box;
858
+ }
859
+
860
+ .chatbot-report-source-select .chatbot-report-source-row {
861
+ display: grid;
862
+ grid-template-columns: minmax(0, 1fr) var(--source-check-size);
863
+ column-gap: var(--source-check-gap);
864
+ align-items: center;
865
+ width: 100%;
866
+ padding: 10px var(--source-row-padding-x);
867
+ border: none;
868
+ border-radius: 6px;
869
+ background: var(--report-card-bg);
870
+ font-size: 13px;
871
+ color: #555;
872
+ cursor: pointer;
873
+ text-align: left;
874
+ transition: background 0.15s;
875
+ box-sizing: border-box;
876
+ }
877
+
878
+ .chatbot-report-source-select .chatbot-report-source-row--doc {
879
+ grid-template-columns: 14px minmax(0, 1fr) var(--source-check-size);
880
+ }
881
+
882
+ .chatbot-report-web-search:hover,
883
+ .chatbot-report-source-row:hover {
884
+ background: #efefef;
885
+ }
886
+
887
+ .chatbot-report-web-search {
888
+ margin-top: 12px;
889
+ }
890
+
891
+ .chatbot-report-source-row__label {
892
+ min-width: 0;
893
+ overflow: hidden;
894
+ text-overflow: ellipsis;
895
+ white-space: nowrap;
896
+ }
897
+
898
+ .chatbot-report-source-select {
899
+ --source-check-size: 20px;
900
+ --source-check-gap: 10px;
901
+ --source-row-padding-x: 12px;
902
+ }
903
+
904
+ .chatbot-report-select-all {
905
+ display: grid;
906
+ grid-template-columns: minmax(0, 1fr) var(--source-check-size);
907
+ column-gap: var(--source-check-gap);
908
+ align-items: center;
909
+ padding: 0 var(--source-row-padding-x);
910
+ margin: 16px 0 10px;
911
+ font-size: 13px;
912
+ color: #555;
913
+ cursor: pointer;
914
+ }
915
+
916
+ .chatbot-report-source-select .chatbot-report-check {
917
+ justify-self: end;
918
+ }
919
+
920
+ .chatbot-report-check {
921
+ flex-shrink: 0;
922
+ box-sizing: border-box;
923
+ width: 20px;
924
+ height: 20px;
925
+ border-radius: 50%;
926
+ border: 2px solid #ccc;
927
+ position: relative;
928
+ transition: all 0.2s;
929
+ cursor: pointer;
930
+ background: #fff;
931
+ }
932
+
933
+ .chatbot-report-check.is-checked {
934
+ border-color: #c53355;
935
+ background-color: #c53355;
936
+ }
937
+
938
+ .chatbot-report-check.is-checked::after {
939
+ content: '';
940
+ position: absolute;
941
+ left: 4px;
942
+ top: 1px;
943
+ width: 5px;
944
+ height: 9px;
945
+ border: solid #fff;
946
+ border-width: 0 2px 2px 0;
947
+ transform: rotate(45deg);
948
+ }
949
+
950
+ .chatbot-report-doc-group {
951
+ margin-top: 14px;
952
+ }
953
+
954
+ .chatbot-report-doc-group .chatbot-report-source-row {
955
+ margin-bottom: 8px;
956
+ }
957
+
958
+ .chatbot-report-doc-group .chatbot-report-source-row:last-child {
959
+ margin-bottom: 0;
960
+ }
961
+
962
+ .chatbot-report-doc-group__header.chatbot-report-source-row {
963
+ font-weight: 600;
964
+ color: var(--report-text);
965
+ }
966
+
967
+ .chatbot-report-center {
968
+ flex: 1;
969
+ height: 100%;
970
+ display: flex;
971
+ flex-direction: column;
972
+ min-height: 0;
973
+ background: #fff;
974
+ border-left: 1px solid var(--report-border);
975
+ border-right: 1px solid var(--report-border);
976
+ border-radius: 2px;
977
+ overflow: hidden;
978
+ }
979
+
980
+ .chatbot-report-center__empty {
981
+ flex: 1;
982
+ display: flex;
983
+ align-items: center;
984
+ justify-content: center;
985
+ min-height: 0;
986
+ padding: 24px;
987
+ color: #999;
988
+ font-size: 14px;
989
+ background: #fff;
990
+ }
991
+
992
+ .chatbot-report-center__preview {
993
+ flex: 1;
994
+ min-height: 0;
995
+ display: flex;
996
+ flex-direction: column;
997
+ overflow: hidden;
998
+ background: #fff;
999
+ }
1000
+
1001
+ .chatbot-report-stream-debug {
1002
+ flex: 1;
1003
+ min-height: 0;
1004
+ display: flex;
1005
+ flex-direction: column;
1006
+ overflow: hidden;
1007
+ background: #fff;
1008
+ }
1009
+
1010
+ .chatbot-report-stream-debug__status {
1011
+ flex-shrink: 0;
1012
+ padding: 8px 16px;
1013
+ font-size: 12px;
1014
+ color: var(--report-accent);
1015
+ border-bottom: 1px solid var(--report-border);
1016
+ }
1017
+
1018
+ .chatbot-report-stream-debug__body {
1019
+ flex: 1;
1020
+ min-height: 0;
1021
+ overflow-y: auto;
1022
+ padding: 20px 28px;
1023
+ font-size: 14px;
1024
+ line-height: 1.6;
1025
+ color: #333;
1026
+ white-space: pre-wrap;
1027
+ word-break: break-word;
1028
+ }
1029
+
1030
+ .chatbot-report-toolbar {
1031
+ display: flex;
1032
+ align-items: center;
1033
+ justify-content: space-between;
1034
+ padding: 10px 16px;
1035
+ flex-shrink: 0;
1036
+ gap: 8px;
1037
+ flex-wrap: wrap;
1038
+ }
1039
+
1040
+ .chatbot-report-btn-outline {
1041
+ border: 1px solid var(--report-accent) !important;
1042
+ color: var(--report-accent) !important;
1043
+ background: #fff !important;
1044
+ border-radius: 4px !important;
1045
+ font-size: 13px !important;
1046
+ height: 32px !important;
1047
+ padding-inline: 14px !important;
1048
+ }
1049
+
1050
+ .chatbot-report-btn-outline:hover {
1051
+ background: var(--report-accent-soft) !important;
1052
+ }
1053
+
1054
+ .chatbot-report-document {
1055
+ flex: 1;
1056
+ overflow-y: auto;
1057
+ padding: 20px 28px 24px;
1058
+ background: #fafafa;
1059
+ min-height: 0;
1060
+ }
1061
+
1062
+ .chatbot-report-document__paper {
1063
+ max-width: 720px;
1064
+ margin: 0 auto;
1065
+ background: #fff;
1066
+ border: 1px solid #eee;
1067
+ padding: 32px 36px 40px;
1068
+ min-height: 100%;
1069
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
1070
+ }
1071
+
1072
+ .chatbot-report-document__title-row {
1073
+ display: flex;
1074
+ align-items: flex-start;
1075
+ gap: 8px;
1076
+ margin-bottom: 20px;
1077
+ font-size: 15px;
1078
+ font-weight: 600;
1079
+ line-height: 1.5;
1080
+ }
1081
+
1082
+ .chatbot-report-document__welcome {
1083
+ text-align: center;
1084
+ padding: 48px 16px 32px;
1085
+ color: var(--report-muted);
1086
+ }
1087
+
1088
+ .chatbot-report-document__welcome h2 {
1089
+ font-size: 22px;
1090
+ font-weight: 600;
1091
+ color: var(--report-text);
1092
+ margin: 0 0 12px;
1093
+ }
1094
+
1095
+ .chatbot-report-messages {
1096
+ display: flex;
1097
+ flex-direction: column;
1098
+ gap: 16px;
1099
+ }
1100
+
1101
+ .chatbot-report-sender {
1102
+ flex-shrink: 0;
1103
+ padding: 12px 16px 14px;
1104
+ border-top: 1px solid #f0f0f0;
1105
+ background: #fff;
1106
+ }
1107
+
1108
+ .chatbot-report-chapter-select {
1109
+ --chapter-check-size: 20px;
1110
+ --chapter-version-width: 52px;
1111
+ margin-top: 8px;
1112
+ }
1113
+
1114
+ .chatbot-report-chapter-header {
1115
+ display: flex;
1116
+ align-items: center;
1117
+ justify-content: space-between;
1118
+ gap: 12px;
1119
+ margin-bottom: 4px;
1120
+ }
1121
+
1122
+ .chatbot-report-chapter-header__title {
1123
+ font-size: 13px;
1124
+ font-weight: 600;
1125
+ color: #1a1a1a;
1126
+ }
1127
+
1128
+ .chatbot-report-chapter-header__all {
1129
+ display: flex;
1130
+ align-items: center;
1131
+ gap: 6px;
1132
+ font-size: 12px;
1133
+ color: #555;
1134
+ cursor: pointer;
1135
+ white-space: nowrap;
1136
+ flex-shrink: 0;
1137
+ }
1138
+
1139
+ .chatbot-report-chapter-list {
1140
+ display: flex;
1141
+ flex-direction: column;
1142
+ }
1143
+
1144
+ .chatbot-report-chapter-row {
1145
+ display: grid;
1146
+ grid-template-columns: var(--chapter-check-size) minmax(0, 1fr) var(--chapter-version-width);
1147
+ column-gap: 8px;
1148
+ align-items: center;
1149
+ padding: 7px 4px;
1150
+ margin: 0 -4px;
1151
+ font-size: 13px;
1152
+ color: #333;
1153
+ border-radius: 6px;
1154
+ cursor: grab;
1155
+ user-select: none;
1156
+ transition: background 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
1157
+ }
1158
+
1159
+ .chatbot-report-chapter-row:hover {
1160
+ background: var(--report-card-bg);
1161
+ }
1162
+
1163
+ .chatbot-report-chapter-row.is-dragging {
1164
+ opacity: 0.5;
1165
+ cursor: grabbing;
1166
+ background: #fff;
1167
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
1168
+ }
1169
+
1170
+ .chatbot-report-chapter-row.is-drag-over {
1171
+ background: #fdf0f2;
1172
+ box-shadow: inset 0 0 0 1px rgba(197, 51, 85, 0.35);
1173
+ }
1174
+
1175
+ .chatbot-report-chapter-row .chatbot-report-check {
1176
+ cursor: pointer;
1177
+ }
1178
+
1179
+ .chatbot-report-chapter-row__label {
1180
+ min-width: 0;
1181
+ overflow: hidden;
1182
+ text-overflow: ellipsis;
1183
+ white-space: nowrap;
1184
+ cursor: pointer;
1185
+ line-height: 1.45;
1186
+ color: #333;
1187
+ }
1188
+
1189
+ .chatbot-report-chapter-row__version {
1190
+ width: var(--chapter-version-width);
1191
+ height: 26px;
1192
+ padding: 0 18px 0 8px;
1193
+ border: 1px solid #e5e5e5;
1194
+ border-radius: 4px;
1195
+ font-size: 12px;
1196
+ color: #333;
1197
+ background-color: #fff;
1198
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath fill='%23999' d='M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z'/%3E%3C/svg%3E");
1199
+ background-repeat: no-repeat;
1200
+ background-position: right 4px center;
1201
+ background-size: 12px;
1202
+ appearance: none;
1203
+ cursor: pointer;
1204
+ justify-self: end;
1205
+ box-sizing: border-box;
1206
+ }
1207
+
1208
+ .chatbot-report-section-title--settings {
1209
+ margin: 20px 0 12px;
1210
+ font-size: 13px;
1211
+ font-weight: 600;
1212
+ color: #1a1a1a;
1213
+ }
1214
+
1215
+ .chatbot-report-radio-group {
1216
+ display: flex;
1217
+ flex-direction: column;
1218
+ align-items: flex-start;
1219
+ gap: 12px;
1220
+ margin: 0 0 20px;
1221
+ }
1222
+
1223
+ .chatbot-report-radio-group :deep(.el-radio) {
1224
+ margin-right: 0;
1225
+ height: auto;
1226
+ }
1227
+
1228
+ .chatbot-report-radio-group :deep(.el-radio__label) {
1229
+ font-size: 13px;
1230
+ color: #333;
1231
+ padding-left: 8px;
1232
+ }
1233
+
1234
+ .chatbot-report-radio-group :deep(.el-radio__inner) {
1235
+ width: 16px;
1236
+ height: 16px;
1237
+ border: 1px solid #d9d9d9;
1238
+ }
1239
+
1240
+ .chatbot-report-radio-group :deep(.el-radio__input.is-checked .el-radio__inner) {
1241
+ border-color: #c53355;
1242
+ background: #c53355;
1243
+ }
1244
+
1245
+ .chatbot-report-radio-group :deep(.el-radio__inner::after) {
1246
+ width: 6px;
1247
+ height: 6px;
1248
+ background-color: #fff;
1249
+ }
1250
+
1251
+ .chatbot-report-generate-btn.el-button {
1252
+ display: flex;
1253
+ width: 100%;
1254
+ height: 40px;
1255
+ border: 1px solid #c53355;
1256
+ color: #c53355;
1257
+ background: #fff;
1258
+ border-radius: 20px;
1259
+ font-size: 14px;
1260
+ font-weight: 500;
1261
+ margin: 0 auto 24px;
1262
+ cursor: pointer;
1263
+ transition: background 0.2s, color 0.2s;
1264
+ }
1265
+
1266
+ .chatbot-report-generate-btn.el-button:hover:not(.is-disabled) {
1267
+ background: #fdf0f2;
1268
+ color: #c53355;
1269
+ border-color: #c53355;
1270
+ }
1271
+
1272
+ .chatbot-report-generate-btn.el-button.is-disabled {
1273
+ opacity: 0.6;
1274
+ cursor: not-allowed;
1275
+ }
1276
+
1277
+ .chatbot-report-history {
1278
+ border-top: 1px solid #f0f0f0;
1279
+ padding-top: 20px;
1280
+ }
1281
+
1282
+ .chatbot-report-history__title {
1283
+ font-size: 13px;
1284
+ font-weight: 600;
1285
+ color: #1a1a1a;
1286
+ margin-bottom: 12px;
1287
+ }
1288
+
1289
+ .chatbot-report-history-list {
1290
+ display: flex;
1291
+ flex-direction: column;
1292
+ gap: 6px;
1293
+ }
1294
+
1295
+ .chatbot-report-history-row {
1296
+ display: grid;
1297
+ grid-template-columns: 4px minmax(0, 1fr);
1298
+ column-gap: 10px;
1299
+ align-items: stretch;
1300
+ cursor: pointer;
1301
+ }
1302
+
1303
+ .chatbot-report-history-item__indicator {
1304
+ width: 4px;
1305
+ border-radius: 2px;
1306
+ background: transparent;
1307
+ align-self: center;
1308
+ min-height: 36px;
1309
+ transition: background 0.15s;
1310
+ }
1311
+
1312
+ .chatbot-report-history-row.is-active .chatbot-report-history-item__indicator {
1313
+ background: #c53355;
1314
+ }
1315
+
1316
+ .chatbot-report-history-item {
1317
+ display: flex;
1318
+ align-items: center;
1319
+ justify-content: space-between;
1320
+ gap: 10px;
1321
+ padding: 12px 14px;
1322
+ border-radius: 12px;
1323
+ transition: background 0.15s;
1324
+ }
1325
+
1326
+ .chatbot-report-history-row.is-active .chatbot-report-history-item {
1327
+ background: #fdf0f3;
1328
+ }
1329
+
1330
+ .chatbot-report-history-row:not(.is-active):hover .chatbot-report-history-item {
1331
+ background: #fafafa;
1332
+ }
1333
+
1334
+ .chatbot-report-history-item__body {
1335
+ flex: 1;
1336
+ min-width: 0;
1337
+ }
1338
+
1339
+ .chatbot-report-history-item__title {
1340
+ font-size: 13px;
1341
+ font-weight: 600;
1342
+ color: #1a1a1a;
1343
+ line-height: 1.4;
1344
+ }
1345
+
1346
+ .chatbot-report-history-item__meta {
1347
+ margin-top: 2px;
1348
+ color: #999;
1349
+ font-size: 12px;
1350
+ line-height: 1.4;
1351
+ }
1352
+
1353
+ .chatbot-report-history-item__menu {
1354
+ flex-shrink: 0;
1355
+ width: 16px;
1356
+ height: 16px;
1357
+ font-size: 16px;
1358
+ color: #c53355;
1359
+ transform: rotate(90deg);
1360
+ cursor: pointer;
1361
+ }
1362
+
1363
+ @media (max-width: 1200px) {
1364
+ .chatbot-report-shell {
1365
+ flex-direction: column;
1366
+ overflow-y: auto;
1367
+ }
1368
+
1369
+ .chatbot-report-shell .left,
1370
+ .chatbot-report-shell .right {
1371
+ width: 100%;
1372
+ min-width: 0;
1373
+ }
1374
+
1375
+ .chatbot-report-shell .center {
1376
+ min-width: 0;
1377
+ min-height: 420px;
1378
+ }
1379
+ }
1380
+ </style>