large-model-component 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 (87) hide show
  1. package/.editorconfig +13 -0
  2. package/.env +2 -0
  3. package/.env.development +2 -0
  4. package/.env.production +2 -0
  5. package/.eslintignore +9 -0
  6. package/.eslintrc.js +7 -0
  7. package/.markdownlint.json +3 -0
  8. package/.markdownlintignore +7 -0
  9. package/.prettierrc.js +8 -0
  10. package/.stylelintignore +10 -0
  11. package/.stylelintrc.js +3 -0
  12. package/CHANGELOG.md +4 -0
  13. package/README.md +57 -0
  14. package/babel.config.js +3 -0
  15. package/commitlint.config.js +3 -0
  16. package/dist/css/248.e455a0b7.css +1 -0
  17. package/dist/css/712.3716bdaf.css +1 -0
  18. package/dist/css/792.3716bdaf.css +1 -0
  19. package/dist/demo.html +1 -0
  20. package/dist/img/ai-chart.167a7713.png +0 -0
  21. package/dist/img/scrol-bg.f446933a.png +0 -0
  22. package/dist/img/zhijing-model.6a81c5a7.png +0 -0
  23. package/dist/large-model-component.common.712.js +73 -0
  24. package/dist/large-model-component.common.712.js.map +1 -0
  25. package/dist/large-model-component.common.js +143384 -0
  26. package/dist/large-model-component.common.js.map +1 -0
  27. package/dist/large-model-component.css +1 -0
  28. package/dist/large-model-component.umd.792.js +73 -0
  29. package/dist/large-model-component.umd.792.js.map +1 -0
  30. package/dist/large-model-component.umd.js +143395 -0
  31. package/dist/large-model-component.umd.js.map +1 -0
  32. package/dist/large-model-component.umd.min.248.js +2 -0
  33. package/dist/large-model-component.umd.min.248.js.map +1 -0
  34. package/dist/large-model-component.umd.min.js +32 -0
  35. package/dist/large-model-component.umd.min.js.map +1 -0
  36. package/docs/.vuepress/config.js +30 -0
  37. package/docs/.vuepress/enhanceApp.js +7 -0
  38. package/docs/.vuepress/styles/palette.styl +3 -0
  39. package/docs/README.md +12 -0
  40. package/docs/comps/README.md +18 -0
  41. package/docs/comps/header.md +100 -0
  42. package/docs/config.md +0 -0
  43. package/docs/logs/README.md +42 -0
  44. package/f2elint.config.js +5 -0
  45. package/jsconfig.json +19 -0
  46. package/package.json +80 -0
  47. package/packages/index.js +27 -0
  48. package/packages/largeModel/contentFold.vue +221 -0
  49. package/packages/largeModel/index.js +2 -0
  50. package/packages/largeModel/index.vue +1703 -0
  51. package/packages/largeModel/pubsub.js +30 -0
  52. package/packages/largeModel/wsconnecter.js +60 -0
  53. package/public/index.html +17 -0
  54. package/src/App.vue +33 -0
  55. package/src/api/user.js +68 -0
  56. package/src/assets/css/app.css +3255 -0
  57. package/src/assets/css/base.css +3 -0
  58. package/src/assets/css/chunk-vendors.css +2071 -0
  59. package/src/assets/css/github-markdown.css +985 -0
  60. package/src/assets/img/ai-chart.png +0 -0
  61. package/src/assets/img/close.png +0 -0
  62. package/src/assets/img/delete.png +0 -0
  63. package/src/assets/img/fontawesome-webfont.912ec66d.svg +2671 -0
  64. package/src/assets/img/home-bg.png +0 -0
  65. package/src/assets/img/link.png +0 -0
  66. package/src/assets/img/message.png +0 -0
  67. package/src/assets/img/qas.png +0 -0
  68. package/src/assets/img/quick_finder_icon1.png +0 -0
  69. package/src/assets/img/quick_finder_icon2.png +0 -0
  70. package/src/assets/img/quick_finder_icon3.png +0 -0
  71. package/src/assets/img/quick_finder_icon4.png +0 -0
  72. package/src/assets/img/quizzer.png +0 -0
  73. package/src/assets/img/scrol-bg.png +0 -0
  74. package/src/assets/img/send.png +0 -0
  75. package/src/assets/img/serviceIcon.png +0 -0
  76. package/src/assets/img/zhijing-model.png +0 -0
  77. package/src/main.js +15 -0
  78. package/src/router/index.js +20 -0
  79. package/src/store/index.js +26 -0
  80. package/src/utils/auth.js +48 -0
  81. package/src/utils/index.js +111 -0
  82. package/src/utils/request.js +103 -0
  83. package/src/utils/spceialistConfig.js +44 -0
  84. package/src/utils/tool.js +4 -0
  85. package/src/utils/utils.js +56 -0
  86. package/src/utils/validate.js +20 -0
  87. package/vue.config.js +42 -0
@@ -0,0 +1,1703 @@
1
+ <template>
2
+ <div>
3
+ <!-- 悬浮入口按钮 -->
4
+ <div class="entrance-btn" @click="open" title="打开智景大模型"></div>
5
+
6
+ <!-- AI 助手弹窗 -->
7
+ <el-dialog
8
+ :visible.sync="dialogVisible"
9
+ width="1200px"
10
+ :show-close="false"
11
+ class="llm-custom-dialog"
12
+ append-to-body
13
+ >
14
+ <div class="dialog-body-container">
15
+ <el-container class="chat-layout">
16
+ <!-- 左侧侧边栏 -->
17
+ <el-aside width="216px" class="sidebar">
18
+ <img src="@/assets/img/zhijing-model.png" alt="" class="top-logo"/>
19
+ <el-button
20
+ type="primary"
21
+ icon="el-icon-plus"
22
+ size="small"
23
+ class="new-chat-btn"
24
+ @click="addRecord"
25
+ :disabled="loading"
26
+ >新建对话
27
+ </el-button>
28
+ <div class="sidebar-section-title">快捷访问</div>
29
+ <div class="quick-access-list">
30
+ <div class="qa-item" v-for="(item, idx) in quickAccessList" :key="item.id || idx"
31
+ @click="handleQuickAccess(item)">
32
+ <img :src="item.img" alt=""/><span>{{ item.name }}</span>
33
+ </div>
34
+ <div v-if="quickAccessList.length === 0 && !isLoadingQuickAccess" class="empty-qa-tip">暂无快捷访问</div>
35
+ </div>
36
+ <div class="sidebar-section-title history-title">历史会话</div>
37
+ <div class="history-search-box" v-if="record.length > 10 || historySearchQuery">
38
+ <el-input v-model="historySearchQuery" placeholder="搜索历史记录..." prefix-icon="el-icon-search"
39
+ size="mini" clearable @clear="resetHistoryAndLoad" @change="resetHistoryAndLoad"></el-input>
40
+ </div>
41
+ <div
42
+ ref="historyScrollContainer"
43
+ class="history-scroll-container"
44
+ style="height: calc(100% - 428px); overflow-y: auto;"
45
+ >
46
+ <el-menu :default-active="activeChatId ? activeChatId.toString() : ''" class="history-menu"
47
+ background-color="transparent" text-color="#606266" active-text-color="#409EFF">
48
+ <el-menu-item v-for="chat in record" :key="chat.id" :index="chat.id.toString()" @click="cutRecord(chat)"
49
+ class="history-menu-item">
50
+ <i class="el-icon-chat-line-square"></i>
51
+ <span slot="title" class="chat-title-span" :title="chat.title">{{ chat.title }}</span>
52
+ <i class="el-icon-delete delete-icon" slot="title" @click.stop="delRecord(chat)" title="删除会话"></i>
53
+ </el-menu-item>
54
+ <div v-if="record.length === 0 && !isLoadingHistory" class="empty-search-tip">
55
+ {{ historySearchQuery ? '暂无相关历史记录' : '暂无历史会话' }}
56
+ </div>
57
+ <div v-if="isLoadingHistory" class="loading-history-tip"><i class="el-icon-loading"></i> 加载中...</div>
58
+ <div v-if="!isLoadingHistory && !hasMoreHistory && record.length > 0" class="no-more-tip">没有更多了
59
+ </div>
60
+ </el-menu>
61
+ </div>
62
+ </el-aside>
63
+
64
+ <!-- 右侧主聊天区 -->
65
+ <el-main class="main-chat-area">
66
+ <div class="close-btn-wrapper" @click="dialogVisible = false"></div>
67
+ <div class="message-list" ref="messageListRef">
68
+ <!-- 欢迎页 -->
69
+ <div v-if="conversation.dialogue.length === 0" class="welcome-screen">
70
+ <div class="welcome-content">
71
+ <img src="@/assets/img/zhijing-model.png" alt="Logo" class="welcome-logo"/>
72
+ <p class="welcome-desc">行业场景图谱领域的专业大模型,依托"场景图谱 + 要素清单 + 资料数据"进行训练优化,<br>实现企业需求与资源的高效对接。
73
+ </p>
74
+ </div>
75
+ <div class="faq-section">
76
+ <div class="faq-header">
77
+ <div class="faq-title"><img src="@/assets/img/qas.png" alt="">常见问题</div>
78
+ <div class="refresh-btn" @click="changeFaq"><i class="el-icon-refresh"
79
+ style="margin-right: 4px;"></i> 换一批
80
+ </div>
81
+ </div>
82
+ <div class="faq-list">
83
+ <div v-for="(q, idx) in faqList" :key="idx" class="faq-item" :title="q.query || q"
84
+ @click="handleQuickQuestion(q)">{{ q.query || q }}
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+
90
+ <!-- 消息列表 -->
91
+ <div class="message-container">
92
+ <div class="message-item" v-for="(item, index) in conversation.dialogue" :key="index"
93
+ :class="item.role">
94
+ <div class="message-bubble">
95
+ <img
96
+ :src="item.role === 'assistant' ? require('@/assets/img/serviceIcon.png') : require('@/assets/img/quizzer.png')"
97
+ alt="" class="avatar"/>
98
+ <div class="bubble-content" :class="item.role">
99
+ <contentFold v-if="item.role === 'assistant'" :content="item.content"></contentFold>
100
+ <div v-else class="user-text" v-html="item.content"></div>
101
+ </div>
102
+ </div>
103
+ <div v-if="item.role === 'assistant' && !item.isStreaming" class="action-bar">
104
+ <el-tooltip content="复制" placement="top"><i class="el-icon-document-copy action-icon"
105
+ @click="copyContent(item.content)"></i></el-tooltip>
106
+ <el-tooltip content="重新生成" placement="top"><i class="el-icon-refresh action-icon"
107
+ @click="regenerateResponse(index)"></i>
108
+ </el-tooltip>
109
+ <el-tooltip content="赞" placement="top"><i class="el-icon-thumb action-icon"
110
+ :class="{ 'active': item.voteStatus === 1 }"
111
+ @click="toggleFeedback(index, 1,item)"></i></el-tooltip>
112
+ <el-tooltip content="踩" placement="top"><i class="el-icon-thumb action-icon rotated"
113
+ :class="{ 'active': item.voteStatus === -1 }"
114
+ @click="toggleFeedback(index, -1,item)"></i>
115
+ </el-tooltip>
116
+ </div>
117
+ </div>
118
+ <div v-if="loading" class="loading-indicator">
119
+ <i class="el-icon-loading"></i> 思考中...
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- 输入区域 -->
125
+ <div class="input-area">
126
+ <div class="input-box">
127
+ <!-- 文件列表展示区:支持显示多个文件 -->
128
+ <div class="file-list" v-if="files.length > 0">
129
+ <div v-for="file in files" :key="file.id" class="file-item">
130
+ <i class="el-icon-close close_icon" @click="closeIcon(file.id)" title="移除文件"></i>
131
+ <div class="file-info">
132
+ <img v-if="file.type === 'image'" :src="file.previewUrl" style="width: 20px; height: 20px;"
133
+ alt="">
134
+ <i v-else class="el-icon-document"></i>
135
+ <strong>{{ file.name }}</strong>
136
+ <div class="status"
137
+ :class="{ completed: file.status === 'completed', error: file.status === 'error', uploading: file.status === 'uploading' }">
138
+ {{ getStatusText(file) }}
139
+ </div>
140
+ <div class="progress-bar" v-if="file.status === 'uploading'">
141
+ <div class="progress" :style="{ width: file.progress + '%' }"></div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <el-input
148
+ ref="questionInput"
149
+ type="textarea"
150
+ :rows="1"
151
+ resize="none"
152
+ :maxlength="200"
153
+ :disabled="loading"
154
+ placeholder="请输入 200 字以内的自然语言描述..."
155
+ v-model="inputContent"
156
+ @keydown.native.enter.prevent="handleEnterKey"
157
+ @focus="isFocused = true"
158
+ @blur="isFocused = false"
159
+ class="custom-input"
160
+ ></el-input>
161
+
162
+ <div class="input-tools">
163
+ <div class="left-tools">
164
+ <el-tooltip content="上传附件 (支持.pdf/.doc/.docx/.txt/.pptx/.ppt/.xls/.xlsx/.pdf/.png/.jpg/.jpeg)"
165
+ placement="top">
166
+ <div class="upload-trigger">
167
+ <i class="el-icon-paperclip tool-icon" :class="{ 'tool-icon-disabled': loading }"></i>
168
+ <input
169
+ type="file"
170
+ id="fileInput"
171
+ @change="handleFileChange"
172
+ accept=".pdf,.png,.jpg,.jpeg"
173
+ :disabled="loading"
174
+ multiple
175
+ />
176
+ </div>
177
+ </el-tooltip>
178
+ </div>
179
+ <el-button
180
+ type="primary"
181
+ class="send-btn"
182
+ :disabled="(!inputContent.trim() && !hasCompletedFile) || hasUploadingFile"
183
+ @click="loading ? stopGeneration() : save()"
184
+ >
185
+ <template v-if="loading">
186
+ <i class="el-icon-close" style="margin-right: 4px;"></i>
187
+ 停止生成
188
+ </template>
189
+ <template v-else>
190
+ <i class="el-icon-position"></i>
191
+ </template>
192
+ </el-button>
193
+ </div>
194
+ </div>
195
+ <div class="footer-disclaimer">模型版本:V2.0.0 | 内容仅供参考,不构成专业建议</div>
196
+ </div>
197
+ </el-main>
198
+ </el-container>
199
+ </div>
200
+ </el-dialog>
201
+ </div>
202
+ </template>
203
+
204
+ <script>
205
+ import {
206
+ conversationPage,
207
+ deleteHistory,
208
+ historyDetail,
209
+ loginCas,
210
+ quickAccess as getQuickAccessApi,
211
+ suggestive,
212
+ votePort
213
+ } from "@/api/user";
214
+ import ContentFold from './contentFold.vue';
215
+
216
+ const TOKEN_KEY = 'Industrial-Access-Token';
217
+ const CAS_LOGIN_URL = 'https://dticts.paas.casicloud.com/cas/login?service=';
218
+
219
+ const ALLOWED_EXTENSIONS = ['pdf', 'doc', 'docx', 'txt', 'pptx', 'ppt', 'xls', 'xlsx', 'jpg', 'jpeg', 'png', 'gif', 'webp'];
220
+ const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10M
221
+ const DEFAULT_ICONS = [
222
+ require("@/assets/img/quick_finder_icon1.png"),
223
+ require("@/assets/img/quick_finder_icon2.png"),
224
+ require("@/assets/img/quick_finder_icon3.png"),
225
+ require("@/assets/img/quick_finder_icon4.png")
226
+ ];
227
+ export default {
228
+ name: 'LLMAssistantDialog',
229
+ components: {ContentFold},
230
+ data() {
231
+ return {
232
+ dialogVisible: false,
233
+ inputContent: '',
234
+ loading: false,
235
+ abortController: null,
236
+ isCancelled: false,
237
+ isFocused: false,
238
+ record: [],
239
+ pageParams: {currentPage: 1, pageSize: 10, total: 0, hasMore: true, historySearchQuery: ""},
240
+ isLoadingHistory: false,
241
+ historySearchQuery: '',
242
+ isSearching: false,
243
+ quickAccessList: [],
244
+ isLoadingQuickAccess: false,
245
+ conversation: {id: '', title: '', userId: '', dialogue: [], createTime: ''},
246
+ activeChatId: null,
247
+ files: [], // 存储所有上传的文件对象
248
+ fileCounter: 0,
249
+ faqList: [],
250
+ messageId: null,
251
+ pendingHistoryUpdate: null,
252
+ };
253
+ },
254
+ computed: {
255
+ hasUploadingFile() {
256
+ return this.files.some(f => f.status === 'uploading' || f.status === 'pending');
257
+ },
258
+ hasCompletedFile() {
259
+ return this.files.some(f => f.status === 'completed');
260
+ },
261
+ hasMoreHistory() {
262
+ return this.pageParams.hasMore && !this.isSearching;
263
+ }
264
+ },
265
+ async mounted() {
266
+ await this.handleCasCallback();
267
+ this.$nextTick(() => {
268
+ this.initScrollListener();
269
+ });
270
+ },
271
+ beforeDestroy() {
272
+ this.removeScrollListener();
273
+ if (this.throttleTimer) {
274
+ clearTimeout(this.throttleTimer);
275
+ this.throttleTimer = null;
276
+ }
277
+ },
278
+ methods: {
279
+ // 处理 CAS 回调:如果 URL 带有 ticket,则用它换取 token
280
+ async handleCasCallback() {
281
+ const urlParams = new URLSearchParams(window.location.search);
282
+ const ticket = urlParams.get('ticket');
283
+ if (!ticket) return;
284
+
285
+ try {
286
+ // serviceUrl 是去掉 ticket 参数后的当前页面地址
287
+ const serviceUrl = window.location.origin + window.location.pathname;
288
+ const res = await loginCas({ ticket, serviceUrl });
289
+ const token = res.data?.data?.token;
290
+ if (token) {
291
+ localStorage.setItem(TOKEN_KEY, token);
292
+ // ticket 换取 token 成功后自动打开弹窗
293
+ this.openDialog();
294
+ }
295
+ } catch (e) {
296
+ console.error('CAS 登录失败:', e);
297
+ } finally {
298
+ // 清除 URL 中的 ticket 参数,避免刷新重复请求
299
+ const cleanUrl = window.location.origin + window.location.pathname;
300
+ window.history.replaceState({}, '', cleanUrl);
301
+ }
302
+ },
303
+
304
+ // 打开组件前先做 CAS 认证
305
+ open() {
306
+ window.location.href = CAS_LOGIN_URL + encodeURIComponent(location.href);
307
+ },
308
+
309
+ // CAS 认证完成后真正打开弹窗(由 handleCasCallback 成功后调用,或 token 已存在时直接调用)
310
+ openDialog() {
311
+ this.dialogVisible = true;
312
+ this.$nextTick(() => {
313
+ this.initData();
314
+ this.initScrollListener();
315
+ });
316
+ },
317
+ async stopGeneration() {
318
+ if (this.abortController && !this.abortController.signal.aborted) {
319
+ console.log('=== 触发停止生成 ===');
320
+
321
+ // 先调用后端取消接口,让后端停止处理
322
+ if (this.messageId) {
323
+ try {
324
+ const token = localStorage.getItem('Industrial-Access-Token');
325
+ const response = await fetch(`${window.location.origin}/gateway/intelligent/graph/talkbot/query/cancel?messageId=${encodeURIComponent(this.messageId)}`, {
326
+ method: 'POST',
327
+ headers: {
328
+ 'Industrial-Access-Token': token
329
+ }
330
+ });
331
+
332
+ if (response.ok) {
333
+ const result = await response.json();
334
+ console.log('后端取消响应:', result);
335
+ } else {
336
+ console.warn('后端取消请求失败:', response.status);
337
+ }
338
+ } catch (err) {
339
+ console.error('发送取消请求失败:', err);
340
+ }
341
+ }
342
+
343
+ // 再中止前端请求
344
+ this.abortController.abort();
345
+
346
+ // 更新 UI
347
+ const lastItem = this.conversation.dialogue[this.conversation.dialogue.length - 1];
348
+ if (lastItem && lastItem.role === 'assistant') {
349
+ lastItem.content += '\n\n⏹ 已停止生成';
350
+ lastItem.isStreaming = false;
351
+ }
352
+
353
+ this.loading = false;
354
+ this.$message.info('已停止生成');
355
+ } else {
356
+ console.log('=== 无进行中的请求可停止 ===');
357
+ }
358
+
359
+ },
360
+ initScrollListener() {
361
+ const container = this.$refs.historyScrollContainer;
362
+ if (container) {
363
+ container.addEventListener('scroll', this.handleHistoryScroll);
364
+ }
365
+ },
366
+ removeScrollListener() {
367
+ const container = this.$refs.historyScrollContainer;
368
+ if (container) {
369
+ container.removeEventListener('scroll', this.handleHistoryScroll);
370
+ }
371
+ },
372
+ async initData() {
373
+ await this.getQuickAccess();
374
+ await this.changeFaq();
375
+ this.resetHistoryAndLoad();
376
+ },
377
+ async getQuickAccess() {
378
+ this.isLoadingQuickAccess = true;
379
+ try {
380
+ const res = await getQuickAccessApi();
381
+ const list = res.data?.data || res.data || [];
382
+ this.quickAccessList = list.map((item, index) => ({img: DEFAULT_ICONS[index % DEFAULT_ICONS.length], ...item}));
383
+ } catch (error) {
384
+ this.quickAccessList = [];
385
+ } finally {
386
+ this.isLoadingQuickAccess = false;
387
+ }
388
+ },
389
+ resetHistoryAndLoad() {
390
+ this.record = [];
391
+ this.pageParams = {
392
+ currentPage: 1,
393
+ pageSize: 10,
394
+ total: 0,
395
+ historySearchQuery: this.historySearchQuery,
396
+ hasMore: true
397
+ };
398
+ this.isSearching = !!this.historySearchQuery;
399
+ this.loadMoreHistory();
400
+ },
401
+ async loadMoreHistory() {
402
+ if (this.isLoadingHistory || !this.pageParams.hasMore) return;
403
+
404
+ this.isLoadingHistory = true;
405
+ try {
406
+ const params = {
407
+ pageSize: this.pageParams.pageSize,
408
+ currentPage: this.pageParams.currentPage,
409
+ keyWord: this.pageParams.historySearchQuery
410
+ };
411
+
412
+ const res = await conversationPage(params);
413
+ const responseData = res.data?.data || res.data;
414
+ const list = responseData?.content || [];
415
+ const total = responseData?.totalElement || 0;
416
+
417
+ this.record = [...this.record, ...list.map(item => ({
418
+ ...item,
419
+ titleCopy: item.title,
420
+ schema: 'readOnly'
421
+ }))];
422
+
423
+ this.pageParams.total = total;
424
+ if (this.record.length >= total) {
425
+ this.pageParams.hasMore = false;
426
+ } else {
427
+ this.pageParams.currentPage++;
428
+ this.pageParams.hasMore = true;
429
+ }
430
+ } catch (error) {
431
+ console.error('加载历史记录失败:', error);
432
+ this.pageParams.hasMore = false;
433
+ this.$message.error('加载历史会话失败');
434
+ } finally {
435
+ this.isLoadingHistory = false;
436
+ }
437
+ },
438
+ handleHistoryScroll(event) {
439
+ if (this.throttleTimer) return;
440
+ this.throttleTimer = setTimeout(() => {
441
+ const target = event.target;
442
+ const {scrollTop, scrollHeight, clientHeight} = target;
443
+
444
+ if (scrollTop + clientHeight >= scrollHeight - 50) {
445
+ if (!this.isLoadingHistory && this.pageParams.hasMore) {
446
+ this.loadMoreHistory();
447
+ }
448
+ }
449
+
450
+ this.throttleTimer = null;
451
+ }, 300);
452
+ },
453
+ changeFaq() {
454
+ suggestive().then(res => {
455
+ this.faqList = res.data?.data || res.data || []
456
+ }).catch(() => {
457
+ });
458
+ },
459
+ async addRecord() {
460
+ if (this.loading) return;
461
+ this.conversation = {id: '', title: '', userId: '', dialogue: [], createTime: ''};
462
+ this.activeChatId = null;
463
+ this.inputContent = '';
464
+ this.files = [];
465
+ this.$refs.questionInput?.focus();
466
+ await this.resetHistoryAndLoad();
467
+ this.scroll();
468
+ },
469
+ cutRecord(item) {
470
+ if (this.loading) return;
471
+ if (this.activeChatId === item.id && this.conversation.dialogue.length > 0) return;
472
+
473
+ this.activeChatId = item.id;
474
+ this.conversation = {...item, dialogue: []};
475
+ this.loading = true;
476
+
477
+ historyDetail(item.id).then((res) => {
478
+ const detailData = res.data?.data || res.data || [];
479
+ this.conversation.dialogue = detailData.map(msg => ({
480
+ ...msg,
481
+ feedback: msg.feedback || null,
482
+ isStreaming: false
483
+ }));
484
+ this.scroll();
485
+ }).catch(err => {
486
+ this.$message.error("加载会话详情失败");
487
+ }).finally(() => {
488
+ this.loading = false;
489
+ });
490
+ },
491
+ delRecord(item) {
492
+ this.$confirm('确认删除该会话吗?', '提示', {
493
+ confirmButtonText: '确定',
494
+ cancelButtonText: '取消',
495
+ type: 'warning'
496
+ }).then(() => {
497
+ deleteHistory(item.id).then(() => {
498
+ this.$message.success('删除成功');
499
+ this.resetHistoryAndLoad();
500
+ if (item.id === this.activeChatId) this.addRecord();
501
+ }).catch(() => {
502
+ this.$message.error('删除失败');
503
+ });
504
+ }).catch(() => {
505
+ });
506
+ },
507
+ handleEnterKey(e) {
508
+ if (!e.shiftKey && !this.loading) this.save();
509
+ },
510
+ handleQuickAccess(item) {
511
+ if (item.link) this.$router.push({path: item.link});
512
+ },
513
+ handleQuickQuestion(item) {
514
+ this.inputContent = typeof item === 'string' ? item : (item.query || '');
515
+ this.save();
516
+ },
517
+
518
+ async save() {
519
+ if (this.abortController && !this.abortController.signal.aborted) {
520
+ console.log('=== 中止进行中的请求 ===');
521
+ this.abortController.abort();
522
+ await new Promise(resolve => setTimeout(resolve, 50));
523
+ }
524
+
525
+ this.isCancelled = false;
526
+ this.loading = true;
527
+ this.abortController = null;
528
+
529
+ this.abortController = new AbortController();
530
+ const signal = this.abortController.signal;
531
+
532
+ signal.addEventListener('abort', () => {
533
+ console.log('=== 取消信号已触发 ===');
534
+ this.isCancelled = true;
535
+ });
536
+
537
+ const completedFiles = this.files.filter(f => f.status === 'completed');
538
+ const trimmedInput = this.inputContent.trim();
539
+
540
+ if (trimmedInput.length > 200) {
541
+ return this.$message.warning('输入内容请控制在 200 字以内!');
542
+ }
543
+ if (!trimmedInput && completedFiles.length === 0) {
544
+ return this.$message.warning('请输入文字描述!');
545
+ }
546
+ if (!trimmedInput && completedFiles.length > 0) {
547
+ return this.$message.warning('上传文件时必须输入文字描述!');
548
+ }
549
+
550
+ this.loading = true;
551
+ const filesToSendIds = new Set(completedFiles.map(f => f.id));
552
+
553
+ const parseFileList = this.files
554
+ .filter(f => f.type === 'document' && f.status === 'completed')
555
+ .map(f => ({ fileId: f.id, fileName: f.name, markdown: f.parsedContent }));
556
+ const imageFiles = this.files
557
+ .filter(f => f.type === 'image' && f.status === 'completed')
558
+ .map(f => f.rawFile);
559
+
560
+ const userMsg = {
561
+ role: 'user',
562
+ content: this.inputContent,
563
+ files: completedFiles.length > 0 ? completedFiles.map(f => f.name) : []
564
+ };
565
+
566
+ const aiMsg = {
567
+ role: 'assistant',
568
+ content: '',
569
+ feedback: null,
570
+ isStreaming: true
571
+ };
572
+
573
+ this.conversation.dialogue.push(userMsg, aiMsg);
574
+ this.scroll();
575
+
576
+ try {
577
+ const token = localStorage.getItem('Industrial-Access-Token');
578
+
579
+ // 构建 FormData
580
+ const formData = new FormData();
581
+ formData.append('conversationId', this.conversation.id);
582
+ formData.append('query', this.inputContent);
583
+ this.messageId = `msg-${Date.now()}`;
584
+ formData.append('messageId', this.messageId);
585
+ if (parseFileList.length > 0) {
586
+ formData.append('parsedFiles', JSON.stringify(parseFileList));
587
+ }
588
+ imageFiles.forEach(file => formData.append('images', file));
589
+
590
+ // 使用 fetch 获取流式响应
591
+ const response = await fetch(`${window.location.origin}/gateway/intelligent/graph/talkbot/query`, {
592
+ // const response = await fetch(`/talkbot/query`, {
593
+ method: 'POST',
594
+ body: formData,
595
+ headers: {
596
+ 'Industrial-Access-Token': token
597
+ },
598
+ signal: signal
599
+ });
600
+
601
+ if (!response.ok) {
602
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
603
+ }
604
+
605
+ // 读取流式响应(直接读取,不解析 SSE 格式)
606
+ const reader = response.body.getReader();
607
+ const decoder = new TextDecoder('utf-8');
608
+
609
+ while (true) {
610
+ const { done, value } = await reader.read();
611
+ if (done) break;
612
+
613
+ const chunk = decoder.decode(value, { stream: true });
614
+ if (chunk) {
615
+ this.appendAnswer(chunk);
616
+ }
617
+ }
618
+
619
+ // 响应完成
620
+ this.finalizeResponse(filesToSendIds);
621
+
622
+ } catch (error) {
623
+ if (error.name === 'AbortError') {
624
+ console.log('请求已中止');
625
+ }else{
626
+ console.error('请求失败:', error);
627
+ this.loading = false;
628
+ const lastItem = this.conversation.dialogue[this.conversation.dialogue.length - 1];
629
+ if (lastItem && lastItem.role === 'assistant') {
630
+ lastItem.content = '回答失败,请重试!';
631
+ lastItem.error = true;
632
+ lastItem.isStreaming = false;
633
+ }
634
+ }
635
+ }finally {
636
+ this.loading = false;
637
+ this.abortController = null;
638
+ this.isCancelled = false;
639
+ this.$nextTick(() => this.scroll());
640
+ }
641
+ },
642
+
643
+ finalizeResponse(filesToSendIds) {
644
+ const lastItem = this.conversation.dialogue[this.conversation.dialogue.length - 1];
645
+ if (lastItem && lastItem.role === 'assistant') {
646
+ lastItem.isStreaming = false;
647
+ }
648
+ this.loading = false;
649
+ this.$nextTick(() => this.scroll());
650
+
651
+ setTimeout(async () => {
652
+ const res = await conversationPage({
653
+ currentPage: 1,
654
+ pageSize: 10,
655
+ keyWord: this.pageParams.historySearchQuery
656
+ });
657
+ const newList = res.data?.data?.content || res.data?.content || [];
658
+ if (newList.length > 0) {
659
+ this.conversation.id = newList[0].id;
660
+ this.conversation.title = newList[0].title;
661
+ this.activeChatId = newList[0].id;
662
+ this.record = newList;
663
+ }
664
+ this.files = this.files.filter(f => !filesToSendIds.has(f.id));
665
+ this.inputContent = '';
666
+ }, 500);
667
+ },
668
+
669
+ appendAnswer(content) {
670
+ const lastIndex = this.conversation.dialogue.length - 1;
671
+ const lastItem = this.conversation.dialogue[lastIndex];
672
+ if (lastItem && lastItem.role === 'assistant') {
673
+ // 使用 $set 确保 Vue 2 响应式更新
674
+ this.$set(this.conversation.dialogue, lastIndex, {
675
+ ...lastItem,
676
+ content: lastItem.content + content
677
+ });
678
+ this.scroll();
679
+ }
680
+ },
681
+ scroll() {
682
+ if (this.dialogVisible) {
683
+ this.$nextTick(() => {
684
+ const container = this.$refs.messageListRef;
685
+ if (container) container.scrollTop = container.scrollHeight;
686
+ });
687
+ }
688
+ },
689
+ copyContent(text) {
690
+ if (!text) return;
691
+ const tempDiv = document.createElement('div');
692
+ tempDiv.innerHTML = text;
693
+ const pureText = tempDiv.textContent || tempDiv.innerText || text;
694
+
695
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
696
+ navigator.clipboard.writeText(pureText).then(() => {
697
+ this.$message.success('已复制');
698
+ }).catch(err => {
699
+ console.error('Clipboard API 失败:', err);
700
+ this.fallbackCopy(pureText);
701
+ });
702
+ } else {
703
+ this.fallbackCopy(pureText);
704
+ }
705
+ },
706
+ fallbackCopy(text) {
707
+ const tempInput = document.createElement('textarea');
708
+ tempInput.value = text;
709
+ document.body.appendChild(tempInput);
710
+ tempInput.select();
711
+ try {
712
+ document.execCommand('copy');
713
+ this.$message.success('已复制');
714
+ } catch (err) {
715
+ this.$message.error('复制失败,请手动复制');
716
+ }
717
+ document.body.removeChild(tempInput);
718
+ },
719
+ regenerateResponse(index) {
720
+ if (this.loading) return;
721
+ const currentMsg = this.conversation.dialogue[index];
722
+ if (!currentMsg || currentMsg.role !== 'assistant') return;
723
+ const prevIndex = index - 1;
724
+ if (prevIndex < 0 || this.conversation.dialogue[prevIndex].role !== 'user') {
725
+ this.$message.warning('找不到对应的问题');
726
+ return;
727
+ }
728
+ const userMsg = this.conversation.dialogue[prevIndex];
729
+ this.$confirm('确定要重新生成回答吗?', '提示', {
730
+ confirmButtonText: '确定',
731
+ cancelButtonText: '取消',
732
+ type: 'warning'
733
+ }).then(() => {
734
+ this.conversation.dialogue = this.conversation.dialogue.slice(0, prevIndex + 1);
735
+ this.inputContent = userMsg.content.replace(/^\[发送了文件:[\s\S]*?\]/, '').trim();
736
+ this.save();
737
+ }).catch(() => {
738
+ });
739
+ },
740
+ async toggleFeedback(index, status, item) {
741
+ const originalStatus = item.voteStatus;
742
+ item.voteStatus = (item.voteStatus === status) ? null : status;
743
+
744
+ try {
745
+ const res = await votePort({
746
+ messageId: item.id || this.messageId + '-assistant',
747
+ voteStatus: item.voteStatus
748
+ });
749
+
750
+ if (res.data.code === 200) {
751
+ this.$message.success(
752
+ item.voteStatus === 1 ? '感谢点赞' :
753
+ item.voteStatus === -1 ? '收到您的批评' : '已取消反馈'
754
+ );
755
+ if (this.activeChatId) {
756
+ await this.refreshCurrentConversation();
757
+ }
758
+ } else {
759
+ item.voteStatus = originalStatus;
760
+ this.$message.error('反馈提交失败,请重试');
761
+ }
762
+ } catch (error) {
763
+ console.error('投票接口调用失败:', error);
764
+ item.voteStatus = originalStatus;
765
+ this.$message.error('网络异常,反馈提交失败');
766
+ }
767
+ },
768
+ async refreshCurrentConversation() {
769
+ if (!this.activeChatId) return;
770
+ try {
771
+ const res = await historyDetail(this.activeChatId);
772
+ const detailData = res.data?.data || res.data || [];
773
+
774
+ this.conversation.dialogue = detailData.map((msg, idx) => {
775
+ const localMsg = this.conversation.dialogue[idx];
776
+ return {
777
+ ...msg,
778
+ isStreaming: localMsg?.isStreaming || false,
779
+ feedback: msg.feedback || null,
780
+ voteStatus: msg.voteStatus !== undefined ? msg.voteStatus :
781
+ (msg.feedback === 'like' ? 1 : msg.feedback === 'dislike' ? -1 : null)
782
+ };
783
+ });
784
+
785
+ this.scroll();
786
+ } catch (error) {
787
+ console.error('刷新会话详情失败:', error);
788
+ }
789
+ },
790
+ handleFileChange(e) {
791
+ if (e.target.files.length) this.processFiles(e.target.files);
792
+ e.target.value = '';
793
+ },
794
+ processFiles(fileList) {
795
+ const docsToParse = [];
796
+ const imagesToUpload = [];
797
+
798
+ for (let i = 0; i < fileList.length; i++) {
799
+ const file = fileList[i];
800
+ const nameParts = file.name.split('.');
801
+ const extension = nameParts.pop().toLowerCase();
802
+
803
+ if (file.size > MAX_FILE_SIZE) {
804
+ this.$message.error(`文件过大(最大 10M):${file.name}`);
805
+ continue;
806
+ }
807
+
808
+ if (!ALLOWED_EXTENSIONS.includes(extension)) {
809
+ this.$message.error(`不支持的格式:${file.name}`);
810
+ continue;
811
+ }
812
+
813
+ const fileId = `file_${Date.now()}_${this.fileCounter++}`;
814
+
815
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(extension)) {
816
+ const objectURL = URL.createObjectURL(file);
817
+ imagesToUpload.push({
818
+ name: file.name,
819
+ rawFile: file,
820
+ status: 'completed',
821
+ type: 'image',
822
+ previewUrl: objectURL
823
+ });
824
+ } else {
825
+ docsToParse.push({
826
+ id: fileId,
827
+ name: file.name,
828
+ rawFile: file,
829
+ status: 'pending',
830
+ type: 'document',
831
+ progress: 0
832
+ });
833
+ }
834
+ }
835
+ this.files.push(...docsToParse, ...imagesToUpload);
836
+ docsToParse.forEach(fileObj => {
837
+ this.uploadAndParseFile(fileObj);
838
+ });
839
+ },
840
+ async uploadAndParseFile(fileObj) {
841
+ const index = this.files.findIndex(f => f.id === fileObj.id);
842
+ if (index === -1) return;
843
+
844
+ this.files[index].status = 'uploading';
845
+ this.files[index].progress = 10;
846
+
847
+ const formData = new FormData();
848
+ formData.append('file', fileObj.rawFile);
849
+ formData.append('fileId', fileObj.id);
850
+
851
+ try {
852
+ const url = '/gateway/intelligent/graph/file/parse';
853
+ const response = await fetch(url, {
854
+ method: 'POST',
855
+ body: formData,
856
+ headers: {
857
+ "Industrial-Access-Token": localStorage.getItem('Industrial-Access-Token')
858
+ },
859
+ });
860
+ if (!response.ok) {
861
+ throw new Error(`网络请求失败:${response.status}`);
862
+ }
863
+
864
+ const result = await response.json();
865
+ console.log('文件解析返回:', result);
866
+ if (result.data && result.data.success === true && result.data.markdown) {
867
+ this.files[index].status = 'completed';
868
+ this.files[index].progress = 100;
869
+ this.files[index].parsedContent = result.data.markdown;
870
+ this.$message.success(`${fileObj.name} 解析完成`);
871
+ } else if (result.data && result.data.success === false) {
872
+ throw new Error(result.data.error || '解析失败');
873
+ } else {
874
+ throw new Error('返回数据格式异常');
875
+ }
876
+
877
+ } catch (error) {
878
+ console.error('文件上传解析错误:', error);
879
+ this.files[index].status = 'error';
880
+ this.files[index].errorMsg = error.message;
881
+ this.$message.error(`${fileObj.name} 解析失败:${error.message}`);
882
+ }
883
+ },
884
+
885
+ getStatusText(file) {
886
+ switch (file.status) {
887
+ case 'pending':
888
+ return '准备中...';
889
+ case 'uploading':
890
+ return `解析中... ${Math.round(file.progress)}%`;
891
+ case 'completed':
892
+ return '✅ 就绪';
893
+ case 'error':
894
+ return `❌ ${file.errorMsg}`;
895
+ default:
896
+ return '';
897
+ }
898
+ },
899
+ closeIcon(fileId) {
900
+ const index = this.files.findIndex(f => f.id === fileId);
901
+ if (index !== -1) this.files.splice(index, 1);
902
+ },
903
+ }
904
+ };
905
+ </script>
906
+
907
+ <style scoped lang="scss">
908
+ $primary-blue: #409EFF;
909
+ $text-regular: #606266;
910
+ $border-color: #EBEEF5;
911
+ ::v-deep .scroll-bar::-webkit-scrollbar {
912
+ width: 6px;
913
+ }
914
+
915
+ ::v-deep .scroll-bar::-webkit-scrollbar-thumb {
916
+ background: rgba(0, 0, 0, 0.1);
917
+ border-radius: 3px;
918
+ }
919
+
920
+ .entrance-btn {
921
+ width: 83px;
922
+ height: 90px;
923
+ position: fixed;
924
+ right: 30px;
925
+ bottom: 100px;
926
+ cursor: pointer;
927
+ background: url("~@/assets/img/ai-chart.png");
928
+ background-size: 100% 100%;
929
+ z-index: 998;
930
+ }
931
+
932
+ .llm-custom-dialog {
933
+ ::v-deep .el-dialog {
934
+ border-radius: 12px;
935
+ overflow: hidden;
936
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.1);
937
+ }
938
+
939
+ ::v-deep .el-dialog__header {
940
+ padding: 0;
941
+ border-bottom: none;
942
+ margin-right: 0;
943
+ background: #fff;
944
+ display: none;
945
+ }
946
+
947
+ ::v-deep .el-dialog__body {
948
+ padding: 0;
949
+ height: 650px;
950
+ display: flex;
951
+ flex-direction: column;
952
+ overflow: hidden;
953
+ }
954
+ }
955
+
956
+ .dialog-body-container {
957
+ height: 100%;
958
+ display: flex;
959
+ flex-direction: column;
960
+ background: #fff;
961
+ }
962
+
963
+ .chat-layout {
964
+ height: 100%;
965
+ }
966
+
967
+ .sidebar {
968
+ height: 100%;
969
+ padding: 20px;
970
+ display: flex;
971
+ flex-direction: column;
972
+ border-radius: 8px 0 0 8px;
973
+ position: relative;
974
+ z-index: 1;
975
+ background: linear-gradient(180deg, rgba(0, 109, 255, 0.08) 0%, rgba(0, 91, 255, 0.18) 100%), url('~@/assets/img/scrol-bg.png') no-repeat bottom center / 100% 280px;
976
+
977
+ .top-logo {
978
+ width: 90px;
979
+ height: 78px;
980
+ margin: 0 auto 20px;
981
+ display: block;
982
+ z-index: 2;
983
+ position: relative;
984
+ }
985
+
986
+ .new-chat-btn {
987
+ width: 100%;
988
+ margin-bottom: 20px;
989
+ border-radius: 8px;
990
+ background-color: transparent;
991
+ border: 1px solid #005BFF;
992
+ color: #005BFF;
993
+
994
+ &:hover {
995
+ background-color: #005BFF;
996
+ color: #fff;
997
+ }
998
+ }
999
+
1000
+ .sidebar-section-title {
1001
+ margin-bottom: 12px;
1002
+ font-family: PingFangSC, PingFang SC;
1003
+ font-weight: 600;
1004
+ font-size: 15px;
1005
+ color: rgba(0, 0, 0, 0.88);
1006
+ z-index: 2;
1007
+ position: relative;
1008
+
1009
+ &.history-title {
1010
+ margin-top: 10px;
1011
+ margin-bottom: 8px;
1012
+ }
1013
+ }
1014
+
1015
+ .quick-access-list {
1016
+ z-index: 2;
1017
+ position: relative;
1018
+ display: flex;
1019
+ flex-direction: column;
1020
+ gap: 5px;
1021
+ max-height: 300px;
1022
+ overflow-y: auto;
1023
+ padding-right: 4px;
1024
+
1025
+ &::-webkit-scrollbar {
1026
+ width: 4px;
1027
+ }
1028
+
1029
+ &::-webkit-scrollbar-track {
1030
+ background: transparent;
1031
+ }
1032
+
1033
+ &::-webkit-scrollbar-thumb {
1034
+ background: rgba(0, 91, 255, 0.2);
1035
+ border-radius: 2px;
1036
+
1037
+ &:hover {
1038
+ background: rgba(0, 91, 255, 0.4);
1039
+ }
1040
+ }
1041
+
1042
+ .qa-item {
1043
+ display: flex;
1044
+ align-items: center;
1045
+ padding: 5px 12px;
1046
+ border-radius: 6px;
1047
+ cursor: pointer;
1048
+ transition: all 0.2s;
1049
+ font-size: 13px;
1050
+ color: $text-regular;
1051
+ flex-shrink: 0;
1052
+
1053
+ img {
1054
+ width: 24px;
1055
+ height: 24px;
1056
+ margin-right: 8px;
1057
+ object-fit: contain;
1058
+ }
1059
+
1060
+ span {
1061
+ white-space: nowrap;
1062
+ overflow: hidden;
1063
+ text-overflow: ellipsis;
1064
+ }
1065
+
1066
+ &:hover {
1067
+ transform: translateY(-2px);
1068
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
1069
+ }
1070
+ }
1071
+
1072
+ .empty-qa-tip {
1073
+ text-align: center;
1074
+ padding: 10px 0;
1075
+ font-size: 12px;
1076
+ color: #909399;
1077
+ font-style: italic;
1078
+ }
1079
+ }
1080
+
1081
+ .history-search-box {
1082
+ margin-bottom: 12px;
1083
+ z-index: 2;
1084
+ position: relative;
1085
+ }
1086
+
1087
+ .history-scroll-container {
1088
+ overflow-y: auto;
1089
+ scrollbar-width: thin;
1090
+ }
1091
+
1092
+ .history-scroll-container::-webkit-scrollbar {
1093
+ width: 6px;
1094
+ }
1095
+
1096
+ .history-scroll-container::-webkit-scrollbar-thumb {
1097
+ background: rgba(0, 0, 0, 0.1);
1098
+ border-radius: 3px;
1099
+ }
1100
+
1101
+ .history-menu {
1102
+ border-right: none;
1103
+ background: transparent;
1104
+ z-index: 2;
1105
+ position: relative;
1106
+ }
1107
+
1108
+ .el-menu-item {
1109
+ height: 40px;
1110
+ line-height: 40px;
1111
+ font-size: 13px;
1112
+ border-radius: 6px;
1113
+ margin-bottom: 4px;
1114
+ padding-left: 15px !important;
1115
+ padding-right: 10px !important;
1116
+ display: flex;
1117
+ align-items: center;
1118
+ justify-content: space-between;
1119
+ overflow: hidden;
1120
+
1121
+ &:hover {
1122
+ background-color: rgba(64, 158, 255, 0.1);
1123
+
1124
+ .delete-icon {
1125
+ opacity: 1;
1126
+ visibility: visible;
1127
+ }
1128
+ }
1129
+
1130
+ &.is-active {
1131
+ background-color: #fff;
1132
+ color: $primary-blue;
1133
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
1134
+
1135
+ .delete-icon {
1136
+ opacity: 1;
1137
+ visibility: visible;
1138
+ }
1139
+ }
1140
+
1141
+ > i.el-icon-chat-line-square {
1142
+ margin-right: 8px;
1143
+ flex-shrink: 0;
1144
+ }
1145
+
1146
+ .chat-title-span {
1147
+ flex: 1;
1148
+ overflow: hidden;
1149
+ text-overflow: ellipsis;
1150
+ white-space: nowrap;
1151
+ display: block;
1152
+ max-width: calc(100% - 40px);
1153
+ }
1154
+
1155
+ .delete-icon {
1156
+ font-size: 14px;
1157
+ color: #F56C6C;
1158
+ cursor: pointer;
1159
+ margin-left: 8px;
1160
+ flex-shrink: 0;
1161
+ opacity: 0;
1162
+ visibility: hidden;
1163
+ transition: all 0.2s ease;
1164
+
1165
+ &:hover {
1166
+ transform: scale(1.1);
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ .loading-history-tip, .no-more-tip, .empty-search-tip {
1172
+ text-align: center;
1173
+ padding: 15px 0;
1174
+ font-size: 12px;
1175
+ color: #909399;
1176
+ font-style: italic;
1177
+ }
1178
+
1179
+ .loading-history-tip i {
1180
+ margin-right: 5px;
1181
+ }
1182
+ }
1183
+
1184
+ .main-chat-area {
1185
+ padding-top: 50px;
1186
+ display: flex;
1187
+ flex-direction: column;
1188
+ background: #fff;
1189
+ position: relative;
1190
+
1191
+ .close-btn-wrapper {
1192
+ width: 24px;
1193
+ height: 24px;
1194
+ position: absolute;
1195
+ top: 20px;
1196
+ right: 20px;
1197
+ z-index: 10;
1198
+ cursor: pointer;
1199
+ background: url("~@/assets/img/close.png");
1200
+ background-size: 100% 100%;
1201
+ }
1202
+
1203
+ .message-list {
1204
+ flex: 1;
1205
+ overflow-y: auto;
1206
+ padding: 20px 60px;
1207
+ scroll-behavior: smooth;
1208
+ display: flex;
1209
+ flex-direction: column;
1210
+
1211
+ .welcome-screen {
1212
+ flex: 1;
1213
+ display: flex;
1214
+ flex-direction: column;
1215
+ align-items: center;
1216
+ justify-content: flex-end;
1217
+ animation: fadeIn 0.5s ease;
1218
+ width: 100%;
1219
+ min-height: 0;
1220
+
1221
+ .welcome-content {
1222
+ text-align: center;
1223
+ margin-bottom: 24px;
1224
+ flex-shrink: 0;
1225
+
1226
+ .welcome-logo {
1227
+ width: 120px;
1228
+ height: 100px;
1229
+ margin-bottom: 12px;
1230
+ }
1231
+
1232
+ .welcome-desc {
1233
+ margin: 0 auto;
1234
+ font-family: PingFangSC, PingFang SC;
1235
+ font-weight: 400;
1236
+ font-size: 14px;
1237
+ color: rgba(0, 0, 0, 0.65);
1238
+ line-height: 1.5;
1239
+ text-align: center;
1240
+ max-width: 600px;
1241
+ display: -webkit-box;
1242
+ -webkit-line-clamp: 3;
1243
+ -webkit-box-orient: vertical;
1244
+ overflow: hidden;
1245
+ }
1246
+ }
1247
+
1248
+ .faq-section {
1249
+ width: 100%;
1250
+ flex-shrink: 0;
1251
+
1252
+ .faq-header {
1253
+ display: flex;
1254
+ justify-content: space-between;
1255
+ align-items: center;
1256
+ margin-bottom: 10px;
1257
+ padding: 0 4px;
1258
+
1259
+ .faq-title {
1260
+ font-size: 14px;
1261
+ font-weight: 600;
1262
+ color: #303133;
1263
+ display: flex;
1264
+ align-items: center;
1265
+
1266
+ img {
1267
+ width: 16px;
1268
+ height: 16px;
1269
+ margin-right: 6px;
1270
+ }
1271
+ }
1272
+
1273
+ .refresh-btn {
1274
+ font-size: 12px;
1275
+ color: #909399;
1276
+ cursor: pointer;
1277
+ display: flex;
1278
+ align-items: center;
1279
+
1280
+ &:hover {
1281
+ color: $primary-blue;
1282
+ }
1283
+ }
1284
+ }
1285
+
1286
+ .faq-list {
1287
+ display: flex;
1288
+ gap: 10px;
1289
+ flex-wrap: wrap;
1290
+ width: 100%;
1291
+
1292
+ .faq-item {
1293
+ flex: 0 0 auto;
1294
+ width: 30%;
1295
+ min-width: 100px;
1296
+ max-width: 220px;
1297
+ padding: 8px 12px;
1298
+ background: #fff;
1299
+ border: 1px solid $border-color;
1300
+ border-radius: 20px;
1301
+ font-size: 12px;
1302
+ color: $text-regular;
1303
+ cursor: pointer;
1304
+ transition: all 0.2s;
1305
+ white-space: nowrap;
1306
+ overflow: hidden;
1307
+ text-overflow: ellipsis;
1308
+
1309
+ &:hover {
1310
+ border-color: $primary-blue;
1311
+ color: $primary-blue;
1312
+ background: #ecf5ff;
1313
+ transform: translateY(-2px);
1314
+ box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
1315
+ }
1316
+ }
1317
+ }
1318
+ }
1319
+ }
1320
+
1321
+ .message-container {
1322
+ width: 100%;
1323
+ max-width: 900px;
1324
+ margin: 0 auto;
1325
+ padding-bottom: 20px;
1326
+ }
1327
+
1328
+ .message-item {
1329
+ display: flex;
1330
+ flex-direction: column;
1331
+ margin-bottom: 24px;
1332
+ width: 100%;
1333
+ justify-content: flex-start;
1334
+ align-items: flex-start;
1335
+
1336
+ &.user {
1337
+ align-items: flex-end;
1338
+
1339
+ .message-bubble {
1340
+ flex-direction: row-reverse;
1341
+
1342
+ .avatar {
1343
+ margin-left: 12px;
1344
+ margin-right: 0;
1345
+ }
1346
+
1347
+ .bubble-content {
1348
+ text-align: left;
1349
+ }
1350
+ }
1351
+
1352
+ .action-bar {
1353
+ margin-left: 0;
1354
+ margin-right: 40px;
1355
+ justify-content: flex-end;
1356
+ }
1357
+ }
1358
+
1359
+ &.assistant {
1360
+ align-items: flex-start;
1361
+
1362
+ .message-bubble {
1363
+ flex-direction: row;
1364
+
1365
+ .avatar {
1366
+ margin-right: 12px;
1367
+ margin-left: 0;
1368
+ }
1369
+ }
1370
+
1371
+ .action-bar {
1372
+ margin-left: 40px;
1373
+ margin-right: 0;
1374
+ justify-content: flex-start;
1375
+ }
1376
+ }
1377
+
1378
+ .message-bubble {
1379
+ display: flex;
1380
+ align-items: flex-start;
1381
+ max-width: 85%;
1382
+
1383
+ .avatar {
1384
+ width: 32px;
1385
+ height: 32px;
1386
+ border-radius: 50%;
1387
+ flex-shrink: 0;
1388
+ object-fit: cover;
1389
+ }
1390
+
1391
+ .bubble-content {
1392
+ flex: 1;
1393
+ font-family: PingFangSC, PingFang SC;
1394
+ font-size: 15px;
1395
+ line-height: 1.6;
1396
+ color: #303133;
1397
+ word-break: break-word;
1398
+
1399
+ &.user {
1400
+ background: rgb(222, 236, 255);
1401
+ padding: 12px 16px;
1402
+ border-radius: 12px 12px 2px 12px;
1403
+ box-shadow: 0px 2px 9px 0px rgba(215, 227, 237, 0.5);
1404
+ color: #303133;
1405
+ white-space: pre-wrap;
1406
+ }
1407
+
1408
+ &.assistant {
1409
+ border-radius: 12px 12px 12px 2px;
1410
+ background: transparent;
1411
+ padding: 12px 16px 12px 0;
1412
+
1413
+ ::v-deep .content-fold-container {
1414
+ width: 100%;
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+
1420
+ .action-bar {
1421
+ display: flex;
1422
+ gap: 16px;
1423
+ margin-top: 6px;
1424
+
1425
+ .action-icon {
1426
+ font-size: 16px;
1427
+ color: #909399;
1428
+ cursor: pointer;
1429
+ transition: color 0.2s;
1430
+ padding: 4px;
1431
+ border-radius: 4px;
1432
+
1433
+ i {
1434
+ font-size: 14px;
1435
+ }
1436
+
1437
+ &:hover {
1438
+ color: $primary-blue;
1439
+ background-color: #ecf5ff;
1440
+ }
1441
+
1442
+ &.active {
1443
+ color: $primary-blue;
1444
+ font-weight: 600;
1445
+
1446
+ i {
1447
+ transform: scale(1.1);
1448
+ }
1449
+ }
1450
+
1451
+ &.disabled {
1452
+ cursor: not-allowed;
1453
+ opacity: 0.5;
1454
+
1455
+ &:hover {
1456
+ background-color: transparent;
1457
+ color: #909399;
1458
+ }
1459
+ }
1460
+
1461
+ &.rotated {
1462
+ transform: rotate(180deg);
1463
+ }
1464
+ }
1465
+ }
1466
+ }
1467
+ }
1468
+
1469
+ .loading-indicator {
1470
+ text-align: left;
1471
+ margin-left: 40px;
1472
+ color: #909399;
1473
+ font-size: 13px;
1474
+ margin-bottom: 20px;
1475
+ display: flex;
1476
+ align-items: center;
1477
+ gap: 8px;
1478
+ }
1479
+
1480
+ .input-area {
1481
+ padding: 20px 60px 30px;
1482
+ background: #fff;
1483
+ flex-shrink: 0;
1484
+
1485
+ .input-box {
1486
+ border: 1px solid $border-color;
1487
+ border-radius: 12px;
1488
+ padding: 12px;
1489
+ background: #fff;
1490
+ transition: border-color 0.3s, box-shadow 0.3s;
1491
+ position: relative;
1492
+
1493
+ &:focus-within {
1494
+ border-color: $primary-blue;
1495
+ box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.1);
1496
+ }
1497
+
1498
+ .custom-input {
1499
+ ::v-deep textarea {
1500
+ border: none;
1501
+ resize: none;
1502
+ padding: 0;
1503
+ font-size: 15px;
1504
+ max-height: 150px;
1505
+ line-height: 1.5;
1506
+
1507
+ &:focus {
1508
+ box-shadow: none;
1509
+ }
1510
+ }
1511
+ }
1512
+
1513
+ .input-tools {
1514
+ display: flex;
1515
+ justify-content: flex-end;
1516
+ align-items: center;
1517
+ margin-top: 10px;
1518
+ }
1519
+
1520
+ .left-tools {
1521
+ display: flex;
1522
+ gap: 10px;
1523
+
1524
+ .upload-trigger {
1525
+ position: relative;
1526
+ cursor: pointer;
1527
+
1528
+ input[type="file"] {
1529
+ position: absolute;
1530
+ left: 0;
1531
+ top: 0;
1532
+ width: 100%;
1533
+ height: 100%;
1534
+ opacity: 0;
1535
+ cursor: pointer;
1536
+ }
1537
+ }
1538
+
1539
+ .tool-icon {
1540
+ font-size: 18px;
1541
+ color: #909399;
1542
+ cursor: pointer;
1543
+ padding: 6px;
1544
+ border-radius: 6px;
1545
+
1546
+ &:hover {
1547
+ background: #f5f7fa;
1548
+ color: #303133;
1549
+ }
1550
+
1551
+ &.tool-icon-disabled {
1552
+ cursor: not-allowed;
1553
+ color: #C0C4CC;
1554
+
1555
+ &:hover {
1556
+ background: transparent;
1557
+ }
1558
+ }
1559
+ }
1560
+ }
1561
+
1562
+ .left-tools:after {
1563
+ position: relative;
1564
+ content: "";
1565
+ left: 2px;
1566
+ top: 5px;
1567
+ width: 1px;
1568
+ height: 20px;
1569
+ background: rgba(0, 0, 0, 0.08);
1570
+ }
1571
+
1572
+ .send-btn {
1573
+ width: auto;
1574
+ min-width: 36px;
1575
+ height: 36px;
1576
+ background: $primary-blue;
1577
+ border: none;
1578
+ display: flex;
1579
+ align-items: center;
1580
+ justify-content: center;
1581
+ font-size: 14px;
1582
+ padding: 0 12px;
1583
+ margin-left: 20px;
1584
+ transition: all 0.2s ease;
1585
+
1586
+ &:hover {
1587
+ background: #66b1ff;
1588
+ }
1589
+
1590
+ &:disabled {
1591
+ background: #dcdfe6;
1592
+ }
1593
+ }
1594
+ }
1595
+
1596
+ .file-list {
1597
+ display: flex;
1598
+ flex-wrap: wrap;
1599
+ gap: 10px;
1600
+ margin-bottom: 10px;
1601
+
1602
+ .file-item {
1603
+ padding: 8px 12px 8px 24px;
1604
+ background: #f5f7fa;
1605
+ border: 1px solid #e4e7ed;
1606
+ border-radius: 6px;
1607
+ display: flex;
1608
+ align-items: center;
1609
+ font-size: 13px;
1610
+ color: $text-regular;
1611
+ position: relative;
1612
+ max-width: 300px;
1613
+ animation: fadeIn 0.3s ease;
1614
+
1615
+ .close_icon {
1616
+ position: absolute;
1617
+ right: 6px;
1618
+ top: 6px;
1619
+ cursor: pointer;
1620
+ color: #909399;
1621
+ font-size: 14px;
1622
+ padding: 2px;
1623
+ border-radius: 50%;
1624
+
1625
+ &:hover {
1626
+ color: #F56C6C;
1627
+ background: #fff;
1628
+ }
1629
+ }
1630
+
1631
+ .file-info {
1632
+ display: flex;
1633
+ flex-direction: column;
1634
+ gap: 4px;
1635
+ padding-right: 20px;
1636
+ width: 100%;
1637
+
1638
+ strong {
1639
+ font-weight: 500;
1640
+ white-space: nowrap;
1641
+ overflow: hidden;
1642
+ text-overflow: ellipsis;
1643
+ display: block;
1644
+ max-width: 240px;
1645
+ }
1646
+
1647
+ .status {
1648
+ font-size: 12px;
1649
+ color: #909399;
1650
+ display: flex;
1651
+ align-items: center;
1652
+ gap: 4px;
1653
+
1654
+ &.completed {
1655
+ color: #67C23A;
1656
+ }
1657
+
1658
+ &.error {
1659
+ color: #F56C6C;
1660
+ }
1661
+
1662
+ &.uploading {
1663
+ color: #E6A23C;
1664
+ }
1665
+ }
1666
+
1667
+ .progress-bar {
1668
+ width: 100%;
1669
+ height: 4px;
1670
+ background: #e4e7ed;
1671
+ border-radius: 2px;
1672
+ overflow: hidden;
1673
+
1674
+ .progress {
1675
+ height: 100%;
1676
+ background: #67C23A;
1677
+ transition: width 0.3s;
1678
+ }
1679
+ }
1680
+ }
1681
+ }
1682
+ }
1683
+
1684
+ .footer-disclaimer {
1685
+ text-align: center;
1686
+ font-size: 12px;
1687
+ color: #C0C4CC;
1688
+ margin-top: 10px;
1689
+ }
1690
+ }
1691
+ }
1692
+
1693
+ @keyframes fadeIn {
1694
+ from {
1695
+ opacity: 0;
1696
+ transform: translateY(5px);
1697
+ }
1698
+ to {
1699
+ opacity: 1;
1700
+ transform: translateY(0);
1701
+ }
1702
+ }
1703
+ </style>