浏览代码

更新对话上传文件后回答

余思翰 1周前
父节点
当前提交
8d0359cebe
共有 4 个文件被更改,包括 717 次插入27 次删除
  1. 23
    0
      llm-ui/src/api/llm/document.js
  2. 10
    1
      llm-ui/src/api/llm/session.js
  3. 337
    7
      llm-ui/src/views/llm/agent/index.vue
  4. 347
    19
      llm-ui/src/views/llm/chat/index.vue

+ 23
- 0
llm-ui/src/api/llm/document.js 查看文件

@@ -1,3 +1,9 @@
1
+/*
2
+ * @Author: ysh
3
+ * @Date: 2025-06-13 10:35:45
4
+ * @LastEditors: 
5
+ * @LastEditTime: 2025-07-22 10:38:57
6
+ */
1 7
 import request from '@/utils/request'
2 8
 
3 9
 // 查询cmc聊天附件列表
@@ -42,3 +48,20 @@ export function delDocument(documentId) {
42 48
     method: 'delete'
43 49
   })
44 50
 }
51
+
52
+
53
+// 上传聊天外部文件
54
+export function uploadDocument(fileList) {
55
+  const formData = new FormData()
56
+  for (let f of fileList) {
57
+    formData.append('fileList', f)
58
+  }
59
+  return request({
60
+    url: '/llm/document/upload',
61
+    method: 'post',
62
+    data: formData,
63
+    headers: {
64
+      'Content-Type': 'multipart/form-data'
65
+    }
66
+  })
67
+}

+ 10
- 1
llm-ui/src/api/llm/session.js 查看文件

@@ -2,7 +2,7 @@
2 2
  * @Author: wrh
3 3
  * @Date: 2025-04-08 14:23:04
4 4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-06-26 10:14:40
5
+ * @LastEditTime: 2025-07-22 15:42:04
6 6
  */
7 7
 import request from '@/utils/request'
8 8
 
@@ -14,3 +14,12 @@ export function getAnswer(question) {
14 14
     params: question
15 15
   })
16 16
 }
17
+
18
+// 查询cmc聊天记录详细
19
+export function getAnswerWithDocument(question) {
20
+  return request({
21
+    url: '/llm/session/answerWithDocument',
22
+    method: 'get',
23
+    params: question
24
+  })
25
+}

+ 337
- 7
llm-ui/src/views/llm/agent/index.vue 查看文件

@@ -1,11 +1,341 @@
1 1
 <!--
2
- * @Author: wrh
2
+ * @Author: ysh
3 3
  * @Date: 2025-07-17 18:16:50
4
- * @LastEditors: wrh
5
- * @LastEditTime: 2025-07-17 18:17:33
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-22 10:01:21
6 6
 -->
7
-<<template>
8
-  <div>
9
-    
7
+<template>
8
+  <div class="agent-container">
9
+    <!-- 左侧智能体列表 -->
10
+    <div class="agent-list">
11
+      <div class="list-header">
12
+        <h3>智能体列表</h3>
13
+        <el-button type="primary" size="small" @click="handleAdd">
14
+          <el-icon>
15
+            <Plus />
16
+          </el-icon>
17
+          新增
18
+        </el-button>
19
+      </div>
20
+
21
+      <!-- 搜索框 -->
22
+      <div class="search-box">
23
+        <el-input v-model="queryParams.agentName" placeholder="请输入智能体名称" clearable @clear="handleQuery"
24
+          @keyup.enter="handleQuery">
25
+          <template #append>
26
+            <el-button @click="handleQuery">
27
+              <el-icon>
28
+                <Search />
29
+              </el-icon>
30
+            </el-button>
31
+          </template>
32
+        </el-input>
33
+      </div>
34
+
35
+      <!-- 智能体列表 -->
36
+      <div class="list-content" v-loading="loading">
37
+        <div v-for="agent in agentList" :key="agent.agentId" class="agent-item"
38
+          :class="{ 'active': selectedAgentId === agent.agentId }" @click="selectAgent(agent)">
39
+          <div class="agent-info">
40
+            <div class="agent-name">{{ agent.agentName }}</div>
41
+            <div class="agent-desc">{{ agent.description || '暂无描述' }}</div>
42
+            <div class="agent-meta">
43
+              <span class="create-time">{{ agent.createTime }}</span>
44
+            </div>
45
+          </div>
46
+          <div class="agent-actions">
47
+            <el-dropdown @command="handleCommand" trigger="click">
48
+              <el-icon class="action-icon">
49
+                <More />
50
+              </el-icon>
51
+              <template #dropdown>
52
+                <el-dropdown-menu>
53
+                  <el-dropdown-item :command="{ action: 'edit', agent }">编辑</el-dropdown-item>
54
+                  <el-dropdown-item :command="{ action: 'delete', agent }" divided>删除</el-dropdown-item>
55
+                </el-dropdown-menu>
56
+              </template>
57
+            </el-dropdown>
58
+          </div>
59
+        </div>
60
+
61
+        <!-- 空状态 -->
62
+        <div v-if="agentList.length === 0 && !loading" class="empty-state">
63
+          <el-empty description="暂无智能体数据">
64
+            <el-button type="primary" @click="handleAdd">创建智能体</el-button>
65
+          </el-empty>
66
+        </div>
67
+      </div>
68
+
69
+      <!-- 分页 -->
70
+      <div class="pagination-wrapper" v-if="total > 0">
71
+        <el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
72
+          :total="total" :page-sizes="[10, 20, 50]" layout="sizes, prev, pager, next" small @size-change="handleQuery"
73
+          @current-change="handleQuery" />
74
+      </div>
75
+    </div>
76
+
77
+    <!-- 右侧详细内容 -->
78
+    <div class="agent-detail">
79
+      <div class="detail-placeholder">
80
+        <div v-if="selectedAgentId">
81
+          <!-- 这里将来显示智能体详细内容 -->
82
+          <div class="coming-soon">
83
+            <el-icon size="48">
84
+              <Document />
85
+            </el-icon>
86
+            <p>智能体详细内容</p>
87
+            <p class="sub-text">当前选中ID: {{ selectedAgentId }}</p>
88
+          </div>
89
+        </div>
90
+        <div v-else class="no-selection">
91
+          <el-icon size="48"><Select /></el-icon>
92
+          <p>请选择一个智能体查看详细信息</p>
93
+        </div>
94
+      </div>
95
+    </div>
10 96
   </div>
11
-</template>>
97
+</template>
98
+
99
+<script setup>
100
+import { ref, reactive, onMounted } from 'vue'
101
+import { ElMessage, ElMessageBox } from 'element-plus'
102
+import { Plus, Search, More, Document, Select } from '@element-plus/icons-vue'
103
+import { listAgent, addAgent, updateAgent, delAgent } from '@/api/llm/agent'
104
+
105
+// 响应式数据
106
+const loading = ref(false)
107
+const agentList = ref([])
108
+const total = ref(0)
109
+const selectedAgentId = ref(null)
110
+
111
+// 查询参数
112
+const queryParams = reactive({
113
+  pageNum: 1,
114
+  pageSize: 20,
115
+  agentName: ''
116
+})
117
+
118
+// 获取智能体列表
119
+const getList = async () => {
120
+  loading.value = true
121
+  try {
122
+    const response = await listAgent(queryParams)
123
+    agentList.value = response.rows || []
124
+    total.value = response.total || 0
125
+  } catch (error) {
126
+    console.error('获取智能体列表失败:', error)
127
+    ElMessage.error('获取智能体列表失败')
128
+  } finally {
129
+    loading.value = false
130
+  }
131
+}
132
+
133
+// 搜索
134
+const handleQuery = () => {
135
+  queryParams.pageNum = 1
136
+  getList()
137
+}
138
+
139
+// 选择智能体
140
+const selectAgent = (agent) => {
141
+  selectedAgentId.value = agent.agentId
142
+  console.log('选中智能体:', agent)
143
+}
144
+
145
+// 新增智能体
146
+const handleAdd = () => {
147
+  ElMessage.info('新增智能体功能待实现')
148
+  // TODO: 打开新增对话框
149
+}
150
+
151
+// 操作菜单处理
152
+const handleCommand = async (command) => {
153
+  const { action, agent } = command
154
+
155
+  if (action === 'edit') {
156
+    ElMessage.info('编辑智能体功能待实现')
157
+    // TODO: 打开编辑对话框
158
+  } else if (action === 'delete') {
159
+    try {
160
+      await ElMessageBox.confirm(
161
+        `确认删除智能体"${agent.agentName}"吗?`,
162
+        '删除确认',
163
+        {
164
+          confirmButtonText: '确认',
165
+          cancelButtonText: '取消',
166
+          type: 'warning'
167
+        }
168
+      )
169
+
170
+      await delAgent(agent.agentId)
171
+      ElMessage.success('删除成功')
172
+
173
+      // 如果删除的是当前选中的智能体,清空选中状态
174
+      if (selectedAgentId.value === agent.agentId) {
175
+        selectedAgentId.value = null
176
+      }
177
+
178
+      getList()
179
+    } catch (error) {
180
+      if (error !== 'cancel') {
181
+        console.error('删除智能体失败:', error)
182
+        ElMessage.error('删除失败')
183
+      }
184
+    }
185
+  }
186
+}
187
+
188
+// 页面加载时获取数据
189
+onMounted(() => {
190
+  getList()
191
+})
192
+</script>
193
+
194
+<style lang="scss" scoped>
195
+.agent-container {
196
+  display: flex;
197
+  height: calc(100vh - 120px);
198
+  background: #f5f5f5;
199
+}
200
+
201
+.agent-list {
202
+  width: 300px;
203
+  background: white;
204
+  border-right: 1px solid #e4e4e4;
205
+  display: flex;
206
+  flex-direction: column;
207
+
208
+  .list-header {
209
+    padding: 16px;
210
+    border-bottom: 1px solid #e4e4e4;
211
+    display: flex;
212
+    justify-content: space-between;
213
+    align-items: center;
214
+
215
+    h3 {
216
+      margin: 0;
217
+      font-size: 16px;
218
+      font-weight: 500;
219
+    }
220
+  }
221
+
222
+  .search-box {
223
+    padding: 16px;
224
+    border-bottom: 1px solid #e4e4e4;
225
+  }
226
+
227
+  .list-content {
228
+    flex: 1;
229
+    overflow-y: auto;
230
+
231
+    .agent-item {
232
+      padding: 12px 16px;
233
+      border-bottom: 1px solid #f0f0f0;
234
+      cursor: pointer;
235
+      transition: all 0.2s;
236
+      display: flex;
237
+      justify-content: space-between;
238
+      align-items: center;
239
+
240
+      &:hover {
241
+        background: #f8f9fa;
242
+      }
243
+
244
+      &.active {
245
+        background: #e6f7ff;
246
+        border-right: 3px solid #1890ff;
247
+      }
248
+
249
+      .agent-info {
250
+        flex: 1;
251
+
252
+        .agent-name {
253
+          font-size: 14px;
254
+          font-weight: 500;
255
+          color: #333;
256
+          margin-bottom: 4px;
257
+        }
258
+
259
+        .agent-desc {
260
+          font-size: 12px;
261
+          color: #666;
262
+          margin-bottom: 4px;
263
+          overflow: hidden;
264
+          text-overflow: ellipsis;
265
+          white-space: nowrap;
266
+        }
267
+
268
+        .agent-meta {
269
+          font-size: 11px;
270
+          color: #999;
271
+
272
+          .create-time {
273
+            margin-right: 8px;
274
+          }
275
+        }
276
+      }
277
+
278
+      .agent-actions {
279
+        .action-icon {
280
+          color: #999;
281
+          font-size: 16px;
282
+          padding: 4px;
283
+          border-radius: 4px;
284
+
285
+          &:hover {
286
+            background: #f0f0f0;
287
+            color: #666;
288
+          }
289
+        }
290
+      }
291
+    }
292
+
293
+    .empty-state {
294
+      padding: 40px 20px;
295
+      text-align: center;
296
+    }
297
+  }
298
+
299
+  .pagination-wrapper {
300
+    padding: 16px;
301
+    border-top: 1px solid #e4e4e4;
302
+    background: white;
303
+  }
304
+}
305
+
306
+.agent-detail {
307
+  flex: 1;
308
+  background: white;
309
+  display: flex;
310
+  align-items: center;
311
+  justify-content: center;
312
+
313
+  .detail-placeholder {
314
+    text-align: center;
315
+    color: #999;
316
+
317
+    .coming-soon {
318
+      .sub-text {
319
+        font-size: 12px;
320
+        margin-top: 8px;
321
+        color: #ccc;
322
+      }
323
+    }
324
+
325
+    .no-selection {
326
+      p {
327
+        margin-top: 16px;
328
+        font-size: 14px;
329
+      }
330
+    }
331
+  }
332
+}
333
+
334
+:deep(.el-pagination) {
335
+  justify-content: center;
336
+
337
+  .el-pagination__sizes {
338
+    margin-right: 8px;
339
+  }
340
+}
341
+</style>

+ 347
- 19
llm-ui/src/views/llm/chat/index.vue 查看文件

@@ -1,8 +1,8 @@
1 1
 <!--
2
- * @Author: wrh
2
+ * @Author: ysh
3 3
  * @Date: 2025-04-07 14:14:05
4
- * @LastEditors: wrh
5
- * @LastEditTime: 2025-07-21 17:06:36
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-22 16:07:39
6 6
 -->
7 7
 <template>
8 8
   <div class="app-container">
@@ -198,6 +198,24 @@
198 198
                     <div class="message-text" v-html="formatMessage(msg.text)"></div>
199 199
                     <div class="message-time">{{ formatMessageTime(msg.time) }}</div>
200 200
                   </div>
201
+                  <!-- 用户消息的文件列表 -->
202
+                  <div v-if="msg.type === 'user' && msg.chatId && msg.fileList && msg.fileList.length > 0"
203
+                    class="message-files">
204
+                    <div class="files-header">
205
+                      <el-icon>
206
+                        <Document />
207
+                      </el-icon>
208
+                      <span>附件 ({{ msg.fileList.length }})</span>
209
+                    </div>
210
+                    <div class="files-list">
211
+                      <div v-for="file in msg.fileList" :key="file.documentId" class="file-item-display">
212
+                        <el-icon class="file-icon">
213
+                          <Document />
214
+                        </el-icon>
215
+                        <span class="file-name">{{ file.path }}</span>
216
+                      </div>
217
+                    </div>
218
+                  </div>
201 219
                 </div>
202 220
               </div>
203 221
               <!-- 加载状态 -->
@@ -222,6 +240,30 @@
222 240
 
223 241
           <!-- 输入框区域 -->
224 242
           <div class="input-container">
243
+            <!-- 文件列表显示区域 -->
244
+            <div v-if="selectedFiles.length > 0 || isUploading" class="file-list-container">
245
+              <div class="file-list-header">
246
+                <span v-if="isUploading">正在上传文件...</span>
247
+                <span v-else>已上传 {{ selectedFiles.length }} 个文件</span>
248
+                <el-icon v-if="isUploading" class="loading-icon">
249
+                  <Loading />
250
+                </el-icon>
251
+              </div>
252
+              <div class="file-list">
253
+                <div v-for="(file, index) in selectedFiles" :key="index" class="file-item">
254
+                  <div class="file-info">
255
+                    <el-icon class="file-icon">
256
+                      <Document />
257
+                    </el-icon>
258
+                    <span class="file-name">{{ file.name }}</span>
259
+                    <span class="file-size">({{ formatFileSize(file.size) }})</span>
260
+                  </div>
261
+                  <el-button v-if="!isUploading" size="small" circle :icon="Delete" class="remove-btn"
262
+                    @click="removeFile(index)" />
263
+                </div>
264
+              </div>
265
+            </div>
266
+
225 267
             <div class="input-wrapper">
226 268
               <el-input v-model="inputMessage" type="textarea" :rows="1" :autosize="{ minRows: 1, maxRows: 6 }"
227 269
                 placeholder="输入您的问题..." @keydown.enter.prevent="handleEnter" @keydown.ctrl.enter="handleCtrlEnter"
@@ -235,6 +277,10 @@
235 277
             <div class="input-tips">
236 278
               <span>按 Enter 发送,Ctrl + Enter 换行</span>
237 279
             </div>
280
+
281
+            <!-- 隐藏的文件选择input -->
282
+            <input ref="fileInput" type="file" multiple style="display: none" @change="handleFileSelect"
283
+              accept=".txt,.doc,.docx,.pdf,.xlsx,.xls,.ppt,.pptx,.md" />
238 284
           </div>
239 285
         </div>
240 286
       </div>
@@ -244,10 +290,11 @@
244 290
 
245 291
 <script setup name=''>
246 292
 import { reactive, toRefs, onBeforeMount, onMounted, nextTick, ref, computed, getCurrentInstance } from 'vue'
247
-import { Plus, Delete, User, ChatDotRound, Paperclip, Promotion, MoreFilled } from '@element-plus/icons-vue'
293
+import { Plus, Delete, User, ChatDotRound, Paperclip, Promotion, MoreFilled, Document, Loading } from '@element-plus/icons-vue'
248 294
 import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
249 295
 import { listChat, addChat, updateChat } from "@/api/llm/chat";
250
-import { getAnswer } from "@/api/llm/session";
296
+import { listDocument, uploadDocument } from "@/api/llm/document";
297
+import { getAnswer, getAnswerWithDocument } from "@/api/llm/session";
251 298
 import { ElMessage, ElMessageBox } from 'element-plus'
252 299
 import useUserStore from '@/store/modules/user'
253 300
 import logoImg from '@/assets/images/logo.png'
@@ -271,6 +318,11 @@ const inputMessage = ref('');
271 318
 const isLoading = ref(false);
272 319
 const messageScrollbar = ref(null);
273 320
 const showNewChatWelcome = ref(false);
321
+const fileInput = ref(null);
322
+const selectedFiles = ref([]);
323
+const isUploading = ref(false);
324
+const documentChartId = ref('');
325
+const messageFileMap = ref(new Map()); // 存储消息对应的文件列表,key为chartId
274 326
 
275 327
 const classifiedRecent = ref({
276 328
   today: [],
@@ -303,7 +355,7 @@ const handleAction = (command) => {
303 355
     deleteTopic(item);
304 356
   }
305 357
 };
306
-
358
+// 删除话题
307 359
 const deleteTopic = async (item) => {
308 360
   try {
309 361
     await ElMessageBox.confirm('确定要删除这个对话吗?', '提示', {
@@ -366,6 +418,10 @@ const loadChatMessages = async (topicId) => {
366 418
   try {
367 419
     const response = await listChat({ topicId: topicId, pageSize: 1000 });
368 420
     chatMessages.value = response.rows || [];
421
+
422
+    // 同时加载具有文件的对话
423
+    await loadMessageFiles();
424
+
369 425
     await nextTick();
370 426
     scrollToBottom();
371 427
   } catch (error) {
@@ -373,6 +429,31 @@ const loadChatMessages = async (topicId) => {
373 429
   }
374 430
 };
375 431
 
432
+const loadMessageFiles = async () => {
433
+  try {
434
+    // 清空之前的文件映射
435
+    messageFileMap.value.clear();
436
+    // 获取所有有chatId的消息
437
+    const messagesWithChartId = chatMessages.value.filter(msg => msg.chatId);
438
+
439
+    // 为每个chartId获取文件列表
440
+    const promises = messagesWithChartId.map(async (msg) => {
441
+      try {
442
+        const fileResponse = await listDocument({ chatId: msg.chatId });
443
+        if (fileResponse.rows && fileResponse.rows.length > 0) {
444
+          messageFileMap.value.set(msg.chatId, fileResponse.rows);
445
+        }
446
+      } catch (error) {
447
+        console.error(`Failed to load files for chatId ${msg.chatId}:`, error);
448
+      }
449
+    });
450
+
451
+    await Promise.all(promises);
452
+  } catch (error) {
453
+    console.error('Failed to load message files:', error);
454
+  }
455
+};
456
+
376 457
 const sendMessage = async () => {
377 458
   if (!inputMessage.value.trim() || isLoading.value) return;
378 459
 
@@ -384,7 +465,8 @@ const sendMessage = async () => {
384 465
     userId: userStore.id,
385 466
     input: inputMessage.value,
386 467
     topicId: currentTopicId.value,
387
-    inputTime: proxy.parseTime(new Date(), '{y}-{m}-{d}')
468
+    inputTime: proxy.parseTime(new Date(), '{y}-{m}-{d}'),
469
+    chatId: documentChartId.value || null
388 470
   };
389 471
 
390 472
   // 添加用户消息到聊天记录
@@ -392,6 +474,15 @@ const sendMessage = async () => {
392 474
   const messageToSend = inputMessage.value;
393 475
   inputMessage.value = '';
394 476
 
477
+  // 暂存文件上传ID,用于后续查询
478
+  const uploadedFileId = documentChartId.value;
479
+
480
+  // 清空文档chartId,确保每次上传只对下一条消息有效
481
+  documentChartId.value = '';
482
+
483
+  // 清空已上传的文件列表
484
+  selectedFiles.value = [];
485
+
395 486
   await nextTick();
396 487
   scrollToBottom();
397 488
 
@@ -418,8 +509,15 @@ const sendMessage = async () => {
418 509
   // 发送消息到后端,得到回答,并更新到数据库
419 510
   try {
420 511
     isLoading.value = true;
421
-    const answer = await getAnswer({ topicId: currentTopicId.value, question: userMessage.input });
422
-    
512
+
513
+    let answer;
514
+    // 判断是否存在附件,存在则使用getAnswerWithDocument API
515
+    if (uploadedFileId) {
516
+      answer = await getAnswerWithDocument({ topicId: currentTopicId.value, chatId: uploadedFileId, question: userMessage.input });
517
+    } else {
518
+      answer = await getAnswer({ topicId: currentTopicId.value, question: userMessage.input });
519
+    }
520
+
423 521
     // 使用Vue的响应式更新方法
424 522
     const messageIndex = chatMessages.value.length - 1;
425 523
     if (messageIndex >= 0) {
@@ -430,10 +528,35 @@ const sendMessage = async () => {
430 528
         outputTime: proxy.parseTime(new Date(), '{y}-{m}-{d}')
431 529
       };
432 530
     }
433
-    
531
+
434 532
     // 保存到数据库
435
-    await addChat(chatMessages.value[messageIndex]);
436
-    
533
+    const savedMessage = await addChat(chatMessages.value[messageIndex]);
534
+
535
+    // 如果保存成功,更新消息的实际ID
536
+    if (savedMessage && savedMessage.chatId) {
537
+      chatMessages.value[messageIndex].chatId = savedMessage.chatId;
538
+    }
539
+
540
+    // 如果当前消息包含文件,使用上传时的ID查询文件列表
541
+    if (uploadedFileId) {
542
+      try {
543
+        const fileResponse = await listDocument({ chatId: uploadedFileId });
544
+        if (fileResponse.rows && fileResponse.rows.length > 0) {
545
+          // 使用消息的chatId作为key存储文件列表
546
+          const messageChatId = chatMessages.value[messageIndex].chatId;
547
+          const keyToUse = messageChatId || uploadedFileId;
548
+          messageFileMap.value.set(keyToUse, fileResponse.rows);
549
+
550
+          // 确保消息有chatId用于显示
551
+          if (!chatMessages.value[messageIndex].chatId) {
552
+            chatMessages.value[messageIndex].chatId = uploadedFileId;
553
+          }
554
+        }
555
+      } catch (error) {
556
+        console.error('Failed to load files for new message:', error);
557
+      }
558
+    }
559
+
437 560
     isLoading.value = false;
438 561
     await nextTick();
439 562
     scrollToBottom();
@@ -452,7 +575,47 @@ const handleCtrlEnter = () => {
452 575
 };
453 576
 
454 577
 const handleFileUpload = () => {
455
-  ElMessage.info('文件上传功能开发中...');
578
+  if (fileInput.value) {
579
+    fileInput.value.click();
580
+  }
581
+};
582
+
583
+const handleFileSelect = async (event) => {
584
+  const files = Array.from(event.target.files);
585
+  if (files.length > 0) {
586
+    // 清空input值,允许选择相同文件
587
+    event.target.value = '';
588
+
589
+    // 立即上传文件
590
+    try {
591
+      isUploading.value = true;
592
+      let res = await uploadDocument(files);
593
+      documentChartId.value = res.chatId;
594
+      // 上传成功后,将文件添加到已上传列表
595
+      selectedFiles.value = [...selectedFiles.value, ...files];
596
+      ElMessage.success(`成功上传 ${files.length} 个文件`);
597
+
598
+    } catch (error) {
599
+      ElMessage.error('文件上传失败');
600
+      console.error('File upload error:', error);
601
+    } finally {
602
+      isUploading.value = false;
603
+    }
604
+  }
605
+};
606
+
607
+const removeFile = (index) => {
608
+  selectedFiles.value.splice(index, 1);
609
+};
610
+
611
+
612
+
613
+const formatFileSize = (bytes) => {
614
+  if (bytes === 0) return '0 B';
615
+  const k = 1024;
616
+  const sizes = ['B', 'KB', 'MB', 'GB'];
617
+  const i = Math.floor(Math.log(bytes) / Math.log(k));
618
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
456 619
 };
457 620
 
458 621
 const scrollToBottom = () => {
@@ -579,12 +742,16 @@ const flatMessages = computed(() => {
579 742
   for (const msg of chatMessages.value) {
580 743
     // 处理用户消息
581 744
     if (msg.input && msg.input.trim()) {
582
-      arr.push({
745
+      const fileList = msg.chatId ? messageFileMap.value.get(msg.chatId) || [] : [];
746
+      const userMessage = {
583 747
         type: 'user',
584 748
         text: msg.input,
585 749
         time: msg.inputTime,
586 750
         id: msg.chatId || (msg.topicId + '_input_' + msg.inputTime),
587
-      })
751
+        chatId: msg.chatId,
752
+        fileList: fileList
753
+      };
754
+      arr.push(userMessage);
588 755
     }
589 756
     // 处理AI回答消息
590 757
     if (msg.output && msg.output.trim()) {
@@ -662,20 +829,20 @@ onMounted(() => {
662 829
           height: 100%;
663 830
           padding: 20px;
664 831
           overflow-y: auto;
665
-          
832
+
666 833
           &::-webkit-scrollbar {
667 834
             width: 6px;
668 835
           }
669
-          
836
+
670 837
           &::-webkit-scrollbar-track {
671 838
             background: #f1f1f1;
672 839
             border-radius: 3px;
673 840
           }
674
-          
841
+
675 842
           &::-webkit-scrollbar-thumb {
676 843
             background: #c1c1c1;
677 844
             border-radius: 3px;
678
-            
845
+
679 846
             &:hover {
680 847
               background: #a8a8a8;
681 848
             }
@@ -799,6 +966,65 @@ onMounted(() => {
799 966
                 text-align: right;
800 967
               }
801 968
             }
969
+
970
+            .message-files {
971
+              margin-top: 8px;
972
+              border: 1px solid #e9ecef;
973
+              border-radius: 8px;
974
+              background-color: #f8f9fa;
975
+              padding: 8px;
976
+
977
+              .files-header {
978
+                display: flex;
979
+                align-items: center;
980
+                gap: 6px;
981
+                font-size: 12px;
982
+                color: #666;
983
+                margin-bottom: 6px;
984
+                font-weight: 500;
985
+
986
+                .el-icon {
987
+                  font-size: 14px;
988
+                }
989
+              }
990
+
991
+              .files-list {
992
+                .file-item-display {
993
+                  display: flex;
994
+                  align-items: center;
995
+                  gap: 6px;
996
+                  padding: 4px 8px;
997
+                  background-color: white;
998
+                  border-radius: 4px;
999
+                  margin-bottom: 4px;
1000
+                  border: 1px solid #e9ecef;
1001
+
1002
+                  &:last-child {
1003
+                    margin-bottom: 0;
1004
+                  }
1005
+
1006
+                  .file-icon {
1007
+                    color: #666;
1008
+                    font-size: 14px;
1009
+                  }
1010
+
1011
+                  .file-name {
1012
+                    flex: 1;
1013
+                    font-size: 12px;
1014
+                    color: #333;
1015
+                    white-space: nowrap;
1016
+                    overflow: hidden;
1017
+                    text-overflow: ellipsis;
1018
+                  }
1019
+
1020
+                  .file-size {
1021
+                    font-size: 11px;
1022
+                    color: #999;
1023
+                    white-space: nowrap;
1024
+                  }
1025
+                }
1026
+              }
1027
+            }
802 1028
           }
803 1029
         }
804 1030
 
@@ -830,6 +1056,98 @@ onMounted(() => {
830 1056
         padding: 16px;
831 1057
         background-color: white;
832 1058
 
1059
+        .file-list-container {
1060
+          margin-bottom: 12px;
1061
+          border: 1px solid #e9ecef;
1062
+          border-radius: 8px;
1063
+          background-color: #f8f9fa;
1064
+
1065
+          .file-list-header {
1066
+            display: flex;
1067
+            justify-content: space-between;
1068
+            align-items: center;
1069
+            padding: 8px 12px;
1070
+            border-bottom: 1px solid #e9ecef;
1071
+            font-size: 14px;
1072
+            font-weight: 500;
1073
+            color: #333;
1074
+
1075
+            .loading-icon {
1076
+              animation: rotate 1s linear infinite;
1077
+              color: #007bff;
1078
+              font-size: 16px;
1079
+            }
1080
+          }
1081
+
1082
+          .file-list {
1083
+            max-height: 200px;
1084
+            overflow-y: auto;
1085
+            padding: 8px;
1086
+
1087
+            .file-item {
1088
+              display: flex;
1089
+              justify-content: space-between;
1090
+              align-items: center;
1091
+              padding: 8px 12px;
1092
+              margin-bottom: 4px;
1093
+              background-color: white;
1094
+              border-radius: 6px;
1095
+              border: 1px solid #e9ecef;
1096
+              transition: all 0.2s ease;
1097
+
1098
+              &:hover {
1099
+                border-color: #007bff;
1100
+                box-shadow: 0 2px 4px rgba(0, 123, 255, 0.1);
1101
+              }
1102
+
1103
+              &:last-child {
1104
+                margin-bottom: 0;
1105
+              }
1106
+
1107
+              .file-info {
1108
+                display: flex;
1109
+                align-items: center;
1110
+                flex: 1;
1111
+                min-width: 0;
1112
+
1113
+                .file-icon {
1114
+                  margin-right: 8px;
1115
+                  color: #666;
1116
+                  font-size: 16px;
1117
+                }
1118
+
1119
+                .file-name {
1120
+                  font-size: 14px;
1121
+                  color: #333;
1122
+                  white-space: nowrap;
1123
+                  overflow: hidden;
1124
+                  text-overflow: ellipsis;
1125
+                  margin-right: 8px;
1126
+                }
1127
+
1128
+                .file-size {
1129
+                  font-size: 12px;
1130
+                  color: #999;
1131
+                  white-space: nowrap;
1132
+                }
1133
+              }
1134
+
1135
+              .remove-btn {
1136
+                color: #dc3545;
1137
+                border: none;
1138
+                background: transparent;
1139
+                padding: 4px;
1140
+                min-height: auto;
1141
+
1142
+                &:hover {
1143
+                  background-color: rgba(220, 53, 69, 0.1);
1144
+                  color: #dc3545;
1145
+                }
1146
+              }
1147
+            }
1148
+          }
1149
+        }
1150
+
833 1151
         .input-wrapper {
834 1152
           position: relative;
835 1153
           border: 1px solid #e9ecef;
@@ -989,4 +1307,14 @@ onMounted(() => {
989 1307
     transform: scale(1);
990 1308
   }
991 1309
 }
1310
+
1311
+@keyframes rotate {
1312
+  from {
1313
+    transform: rotate(0deg);
1314
+  }
1315
+
1316
+  to {
1317
+    transform: rotate(360deg);
1318
+  }
1319
+}
992 1320
 </style>

正在加载...
取消
保存