Bladeren bron

新增智能体详细页面

余思翰 4 dagen geleden
bovenliggende
commit
5b1e07b85a

+ 1
- 0
llm-ui/src/assets/icons/svg/robot.svg Bestand weergeven

@@ -0,0 +1 @@
1
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1753773517876" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11912" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M512 512m-512 0a512 512 0 1 0 1024 0 512 512 0 1 0-1024 0Z" fill="#FFFFFF" p-id="11913"></path><path d="M512 96.533333m-50.826667 0a50.826667 50.826667 0 1 0 101.653334 0 50.826667 50.826667 0 1 0-101.653334 0Z" fill="#FA759E" p-id="11914"></path><path d="M512 158.053333c-33.92 0-61.493333-27.6-61.493333-61.493333S478.08 35.04 512 35.04s61.493333 27.6 61.493333 61.493333S545.92 158.053333 512 158.053333z m0-101.68c-22.16 0-40.16 18.026667-40.16 40.16S489.866667 136.693333 512 136.693333c22.16 0 40.16-18.026667 40.16-40.16S534.16 56.373333 512 56.373333z" fill="#2B2E63" p-id="11915"></path><path d="M501.333333 147.386667h21.333334v53.546666h-21.333334z" fill="#2B2E63" p-id="11916"></path><path d="M571.946667 246.32c0-25.066667-26.853333-45.386667-59.946667-45.386667s-59.946667 20.32-59.946667 45.386667" fill="#0089EF" p-id="11917"></path><path d="M593.493333 250.533333h-21.333333c0-23.68-26.986667-42.96-60.16-42.96s-60.16 19.28-60.16 42.96h-21.333333c0-35.466667 36.56-64.293333 81.493333-64.293333s81.493333 28.853333 81.493333 64.293333z" fill="#2B2E63" p-id="11918"></path><path d="M137.6 702.48c-50.133333 0-90.773333-53.68-90.773333-119.893333S87.466667 462.666667 137.6 462.666667M886.4 702.48c50.133333 0 90.773333-53.68 90.773333-119.893333S936.533333 462.666667 886.4 462.666667" fill="#0089EF" p-id="11919"></path><path d="M674.746667 916.96H349.253333c-116.4 0-211.653333-95.253333-211.653333-211.653333v-196.453334c0-116.4 95.253333-211.653333 211.653333-211.653333h325.493334c116.4 0 211.653333 95.253333 211.653333 211.653333v196.453334c0 116.426667-95.253333 211.653333-211.653333 211.653333z" fill="#6DC9F7" p-id="11920"></path><path d="M137.6 713.146667c-55.946667 0-101.44-58.56-101.44-130.56s45.52-130.56 101.44-130.56v21.333333c-44.186667 0-80.106667 49.013333-80.106667 109.226667 0 60.24 35.946667 109.226667 80.106667 109.226666v21.333334zM886.4 713.146667v-21.333334c44.186667 0 80.106667-49.013333 80.106667-109.226666 0-60.24-35.946667-109.226667-80.106667-109.226667v-21.333333c55.946667 0 101.44 58.56 101.44 130.56 0 71.973333-45.493333 130.56-101.44 130.56z" fill="#2B2E63" p-id="11921"></path><path d="M644.693333 927.626667h-265.413333c-139.146667 0-252.373333-113.2-252.373333-252.373334v-183.04c0-139.146667 113.2-252.373333 252.373333-252.373333h265.413333c139.146667 0 252.373333 113.2 252.373334 252.373333v183.04c0 139.173333-113.2 252.373333-252.373334 252.373334zM379.306667 261.2c-127.386667 0-231.04 103.653333-231.04 231.04v183.04c0 127.386667 103.653333 231.04 231.04 231.04h265.413333c127.386667 0 231.04-103.653333 231.04-231.04v-183.04c0-127.386667-103.653333-231.04-231.04-231.04h-265.413333z" fill="#2B2E63" p-id="11922"></path><path d="M615.173333 786.8h-206.346666c-87.36 0-158.853333-71.493333-158.853334-158.853333v-89.92c0-87.36 71.493333-158.853333 158.853334-158.853334h206.346666c87.36 0 158.853333 71.493333 158.853334 158.853334v89.92c0 87.386667-71.493333 158.853333-158.853334 158.853333z" fill="#FFD7E5" p-id="11923"></path><path d="M390.16 525.546667m-42.4 0a42.4 42.4 0 1 0 84.8 0 42.4 42.4 0 1 0-84.8 0Z" fill="#2B2E63" p-id="11924"></path><path d="M633.84 525.546667m-42.4 0a42.4 42.4 0 1 0 84.8 0 42.4 42.4 0 1 0-84.8 0Z" fill="#2B2E63" p-id="11925"></path><path d="M512 690.373333c-30.693333 0-58.373333-12.88-74.08-34.453333-3.466667-4.773333-2.4-11.44 2.346667-14.906667 4.773333-3.466667 11.44-2.4 14.906666 2.346667 11.493333 15.84 33.28 25.653333 56.8 25.653333s45.306667-9.84 56.8-25.653333c3.466667-4.773333 10.133333-5.813333 14.906667-2.346667 4.773333 3.466667 5.813333 10.133333 2.346667 14.906667-15.653333 21.573333-43.333333 34.453333-74.026667 34.453333z" fill="#2B2E63" p-id="11926"></path></svg>

+ 955
- 0
llm-ui/src/views/llm/agent/AgentDetail.vue Bestand weergeven

@@ -0,0 +1,955 @@
1
+<!--
2
+ * @Author: wrh
3
+ * @Date: 2025-01-01 00:00:00
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-29 15:42:41
6
+-->
7
+<template>
8
+  <div class="agent-detail-container" v-loading="loading">
9
+    <div v-if="agentInfo" class="detail-content">
10
+      <!-- 智能体头部信息 -->
11
+      <div class="agent-header">
12
+        <div class="agent-basic-info">
13
+          <h2 class="agent-title">{{ agentInfo.agentName }}</h2>
14
+          <p class="agent-description">{{ agentInfo.description || '暂无描述' }}</p>
15
+          <div class="agent-meta-info">
16
+            <span class="meta-item">
17
+              <el-icon>
18
+                <Calendar />
19
+              </el-icon>
20
+              创建时间:{{ agentInfo.createTime }}
21
+            </span>
22
+          </div>
23
+        </div>
24
+      </div>
25
+
26
+      <!-- 对话区域 -->
27
+      <div class="chat-section">
28
+        <!-- 话题列表 -->
29
+        <div v-if="topicList.length > 0" class="topic-list">
30
+          <div class="topic-header">
31
+            <h4>历史对话</h4>
32
+          </div>
33
+          <div class="topic-content">
34
+            <div v-for="topic in topicList" :key="topic.topicId" class="topic-item"
35
+              :class="{ 'active': currentTopicId === topic.topicId }" @click="selectTopic(topic)">
36
+              <div class="topic-info">
37
+                <div class="topic-name">{{ topic.topic }}</div>
38
+                <div class="topic-time">{{ topic.createTime }}</div>
39
+              </div>
40
+            </div>
41
+          </div>
42
+        </div>
43
+
44
+        <!-- 对话内容区域 -->
45
+        <div class="chat-content">
46
+          <div class="section-header">
47
+            <h3>{{ chatTitle }}</h3>
48
+            <div class="header-actions">
49
+              <el-button v-if="currentTopicId" size="small" type="primary" @click="startNewChat">
50
+                <el-icon>
51
+                  <Plus />
52
+                </el-icon>
53
+                新建对话
54
+              </el-button>
55
+              <el-button v-if="!currentTopicId && chatMessages.length > 0" size="small" @click="clearChat">
56
+                清空对话
57
+              </el-button>
58
+            </div>
59
+          </div>
60
+
61
+          <!-- 聊天消息列表 -->
62
+          <div class="chat-messages" ref="messagesContainer">
63
+            <!-- 开场白 -->
64
+            <div v-if="openingMessage && !currentTopicId" class="message-item assistant-message">
65
+              <div class="message-avatar">
66
+                <svg-icon icon-class="robot" />
67
+              </div>
68
+              <div class="message-content">
69
+                <div class="message-text">{{ openingMessage }}</div>
70
+              </div>
71
+            </div>
72
+
73
+            <!-- 文件上传提示消息 -->
74
+            <div v-if="openingMessage && !currentTopicId" class="message-item assistant-message">
75
+              <div class="message-avatar">
76
+                <svg-icon icon-class="robot" />
77
+              </div>
78
+              <div class="message-content">
79
+                <div class="message-upload-area">
80
+                  <p class="upload-tip">请上传您需要分析的招标文件(单个文件):</p>
81
+                  <el-upload class="inline-upload" :action="uploadAction" :multiple="false" :auto-upload="false"
82
+                    :file-list="chatFileList" :on-change="handleChatFileChange" :before-upload="beforeUpload"
83
+                    :show-file-list="true" :limit="1">
84
+                    <el-button type="primary" size="small">
85
+                      <el-icon>
86
+                        <Upload />
87
+                      </el-icon>
88
+                      选择文件
89
+                    </el-button>
90
+                  </el-upload>
91
+                  <div v-if="chatFileList.length > 0" class="chat-upload-actions">
92
+                    <el-button size="small" type="success" @click="submitChatUpload">
93
+                      <el-icon>
94
+                        <Check />
95
+                      </el-icon>
96
+                      确认上传
97
+                    </el-button>
98
+                  </div>
99
+                </div>
100
+              </div>
101
+            </div>
102
+
103
+            <!-- 对话消息 -->
104
+            <div v-for="(message, index) in chatMessages" :key="index" class="message-item" :class="[
105
+              message.role === 'user' ? 'user-message' : 'assistant-message',
106
+              message.isFileInfo ? 'file-info-message' : ''
107
+            ]">
108
+              <div class="message-avatar">
109
+                <el-icon v-if="message.role === 'user'">
110
+                  <User />
111
+                </el-icon>
112
+                <el-icon v-else>
113
+                  <svg-icon icon-class="robot" />
114
+                </el-icon>
115
+              </div>
116
+              <div class="message-content">
117
+                <div v-if="message.isHtml" class="message-text" v-html="message.content"></div>
118
+                <div v-else class="message-text">{{ message.content }}</div>
119
+                <div class="message-time">{{ message.timestamp }}</div>
120
+              </div>
121
+            </div>
122
+
123
+            <!-- 正在输入指示器 -->
124
+            <div v-if="isTyping" class="message-item assistant-message">
125
+              <div class="message-avatar">
126
+                <svg-icon icon-class="robot" />
127
+              </div>
128
+              <div class="message-content">
129
+                <div class="typing-indicator">
130
+                  <span></span>
131
+                  <span></span>
132
+                  <span></span>
133
+                </div>
134
+              </div>
135
+            </div>
136
+          </div>
137
+
138
+          <!-- 输入区域 -->
139
+          <div class="chat-input-area">
140
+            <el-input v-model="inputMessage" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }"
141
+              placeholder="请输入您的问题..." @keyup.ctrl.enter="sendMessage" />
142
+            <div class="input-actions">
143
+              <span class="input-tip">Ctrl + Enter 发送</span>
144
+              <el-button type="primary" @click="sendMessage" :disabled="!inputMessage.trim() || isTyping">
145
+                发送
146
+              </el-button>
147
+            </div>
148
+          </div>
149
+        </div>
150
+      </div>
151
+    </div>
152
+
153
+    <!-- 空状态 -->
154
+    <div v-else class="empty-state">
155
+      <el-icon size="48"><Select /></el-icon>
156
+      <p>请选择一个智能体查看详细信息</p>
157
+    </div>
158
+  </div>
159
+</template>
160
+
161
+<script setup>
162
+import { ref, reactive, watch, nextTick, computed, getCurrentInstance } from 'vue';
163
+import { ElMessage } from 'element-plus';
164
+import { getAgent, opening, uploadFile } from '@/api/llm/agent';
165
+import { answer } from '@/api/llm/mcp';
166
+import { listTopic, getTopic, delTopic, addTopic, updateTopic } from "@/api/llm/topic";
167
+import { listChat, addChat, updateChat } from "@/api/llm/chat";
168
+import { listDocument, uploadDocument } from "@/api/llm/document";
169
+import useUserStore from '@/store/modules/user'
170
+
171
+const { proxy } = getCurrentInstance()
172
+
173
+// Props
174
+const props = defineProps({
175
+  agentId: {
176
+    type: [String, Number],
177
+    default: null
178
+  }
179
+})
180
+
181
+const userStore = useUserStore()
182
+
183
+// 响应式数据
184
+const loading = ref(false)
185
+const agentInfo = ref(null)
186
+const openingMessage = ref('')
187
+const chatMessages = ref([])
188
+const inputMessage = ref('')
189
+const isTyping = ref(false)
190
+const messagesContainer = ref(null)
191
+const topicList = ref([])
192
+const currentTopicId = ref(null)
193
+const chatTitle = ref('智能体新对话')
194
+
195
+// 聊天内文件上传相关
196
+const chatFileList = ref([]) // 聊天内的文件列表
197
+const uploadAction = computed(() => '/llm/agent/upload')
198
+
199
+// 监听agentId变化
200
+watch(
201
+  () => props.agentId,
202
+  (newAgentId) => {
203
+    if (newAgentId) {
204
+      loadAgentDetail(newAgentId)
205
+    } else {
206
+      agentInfo.value = null
207
+      openingMessage.value = ''
208
+      chatMessages.value = []
209
+      chatFileList.value = []
210
+      currentTopicId.value = null
211
+      chatTitle.value = '智能体新对话'
212
+    }
213
+  },
214
+  { immediate: true }
215
+)
216
+
217
+// 加载智能体详细信息
218
+const loadAgentDetail = async (agentId) => {
219
+  loading.value = true
220
+  try {
221
+    // 获取智能体详细信息
222
+    const response = await getAgent(agentId)
223
+    agentInfo.value = response.data
224
+
225
+    // 获取开场白
226
+    if (agentInfo.value?.agentName) {
227
+      const res = await opening(agentInfo.value.agentName)
228
+      openingMessage.value = res.data.resultContent;
229
+    }
230
+    // 查询当前智能体的话题列表
231
+    await loadTopic()
232
+    // 自动滚动到底部
233
+    nextTick(() => {
234
+      scrollToBottom()
235
+    })
236
+  } catch (error) {
237
+    console.error('加载智能体详细信息失败:', error)
238
+    ElMessage.error('加载智能体信息失败')
239
+  } finally {
240
+    loading.value = false
241
+  }
242
+}
243
+// 加载选择智能体的话题列表
244
+const loadTopic = async () => {
245
+  const res = await listTopic({ agentId: props.agentId })
246
+  if (res.rows.length > 0) {
247
+    topicList.value = res.rows
248
+  }
249
+}
250
+
251
+// 选择话题
252
+const selectTopic = async (topic) => {
253
+  console.log('选择话题:', topic)
254
+  currentTopicId.value = topic.topicId
255
+  chatTitle.value = topic.topic
256
+  try {
257
+    // 加载该话题下的聊天记录
258
+    const res = await listChat({ topicId: topic.topicId });
259
+    if (res.rows.length > 0) {
260
+      // 处理聊天消息格式
261
+      const messages = []
262
+      for (const chat of res.rows) {
263
+        // 加载该聊天记录相关的文件,优先显示
264
+        if (chat.chatId) {
265
+          try {
266
+            const fileResponse = await listDocument({ chatId: chat.chatId });
267
+            if (fileResponse.rows && fileResponse.rows.length > 0) {
268
+              const fileNames = fileResponse.rows.map(doc => doc.path).join(', ')
269
+              // 先添加文件信息消息
270
+              messages.push({
271
+                role: 'assistant',
272
+                content: `📎 相关文件: ${fileNames}`,
273
+                timestamp: chat.inputTime || new Date(chat.createTime).toLocaleTimeString(),
274
+                isHtml: false,
275
+                isFileInfo: true // 标记为文件信息消息
276
+              })
277
+            }
278
+          } catch (error) {
279
+            console.warn('加载文件信息失败:', error)
280
+          }
281
+        }
282
+        // 添加用户输入消息(如果存在)
283
+        if (chat.input) {
284
+          messages.push({
285
+            role: 'user',
286
+            content: chat.input,
287
+            timestamp: chat.inputTime || new Date(chat.createTime).toLocaleTimeString(),
288
+            isHtml: false
289
+          })
290
+        }
291
+        // 添加助手回复消息
292
+        if (chat.output) {
293
+          messages.push({
294
+            role: 'assistant',
295
+            content: formatContentLinks(chat.output),
296
+            timestamp: chat.outputTime || new Date(chat.createTime).toLocaleTimeString(),
297
+            isHtml: true // 历史消息可能包含HTML内容
298
+          })
299
+        }
300
+      }
301
+      // 更新聊天消息
302
+      chatMessages.value = messages
303
+      // 滚动到底部
304
+      nextTick(() => {
305
+        scrollToBottom()
306
+      })
307
+      ElMessage.success(`已加载话题"${topic.topic}"的对话记录`)
308
+    } else {
309
+      chatMessages.value = []
310
+      ElMessage.info('该话题暂无对话记录')
311
+    }
312
+  } catch (error) {
313
+    console.error('加载话题对话记录失败:', error)
314
+    ElMessage.error('加载对话记录失败')
315
+  }
316
+}
317
+
318
+// 发送消息
319
+const sendMessage = async () => {
320
+  const message = inputMessage.value.trim()
321
+  if (!message || isTyping.value) return
322
+
323
+  // 添加用户消息
324
+  const userMessage = {
325
+    role: 'user',
326
+    content: message,
327
+    timestamp: new Date().toLocaleTimeString(),
328
+    isHtml: false // 用户消息不使用HTML渲染
329
+  }
330
+  chatMessages.value.push(userMessage)
331
+  inputMessage.value = ''
332
+  isTyping.value = true
333
+
334
+  nextTick(() => {
335
+    scrollToBottom()
336
+  })
337
+
338
+  try {
339
+    // 调用智能体回答API
340
+    const response = await answer({
341
+      query: message,
342
+      agentName: agentInfo.value.agentName
343
+    })
344
+
345
+    // 添加助手回复
346
+    const assistantMessage = {
347
+      role: 'assistant',
348
+      content: response.resultContent || '抱歉,我暂时无法回答这个问题。',
349
+      timestamp: new Date().toLocaleTimeString(),
350
+      isHtml: false // 普通聊天消息不使用HTML渲染
351
+    }
352
+    chatMessages.value.push(assistantMessage)
353
+  } catch (error) {
354
+    console.error('发送消息失败:', error)
355
+    ElMessage.error('发送消息失败')
356
+
357
+    // 添加错误消息
358
+    const errorMessage = {
359
+      role: 'assistant',
360
+      content: '抱歉,发生了一些错误,请稍后再试。',
361
+      timestamp: new Date().toLocaleTimeString(),
362
+      isHtml: false
363
+    }
364
+    chatMessages.value.push(errorMessage)
365
+  } finally {
366
+    isTyping.value = false
367
+    nextTick(() => {
368
+      scrollToBottom()
369
+    })
370
+  }
371
+}
372
+
373
+// 滚动到底部
374
+const scrollToBottom = () => {
375
+  if (messagesContainer.value) {
376
+    messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
377
+  }
378
+}
379
+
380
+// 聊天内文件上传相关方法
381
+const handleChatFileChange = (file, fileList) => {
382
+  // 只保留最新选择的文件(限制为单文件)
383
+  chatFileList.value = fileList.slice(-1)
384
+}
385
+
386
+const beforeUpload = (file) => {
387
+  const isValidType = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'text/plain'].includes(file.type)
388
+  const isLt10M = file.size / 1024 / 1024 < 10
389
+
390
+  if (!isValidType) {
391
+    ElMessage.error('只支持 PDF、DOC、DOCX、TXT 格式的文件!')
392
+    return false
393
+  }
394
+  if (!isLt10M) {
395
+    ElMessage.error('单个文件大小不能超过 10MB!')
396
+    return false
397
+  }
398
+  return false // 阻止自动上传
399
+}
400
+// 聊天内文件上传提交
401
+const submitChatUpload = async () => {
402
+  if (chatFileList.value.length === 0) {
403
+    ElMessage.warning('请先选择文件')
404
+    return
405
+  }
406
+  try {
407
+    const file = chatFileList.value[0].raw // 只取第一个文件
408
+    const fileName = file.name;
409
+    const response = await uploadFile(file, agentInfo.value.agentName)
410
+    const fileRes = await uploadDocument([file]); // 保存上传的文件到数据库
411
+    const chatId = fileRes.data.chatId; //获取保存后的chatId
412
+    // 解析返回的数据
413
+    let assistantContent = '文件上传成功!'
414
+    if (response.data && response.data.resultContent) {
415
+      // 格式化链接:在href前加上基础API地址
416
+      assistantContent = formatContentLinks(response.data.resultContent)
417
+    }
418
+
419
+    // 添加上传成功的消息到聊天记录
420
+    const uploadMessage = {
421
+      role: 'assistant',
422
+      content: assistantContent,
423
+      timestamp: new Date().toLocaleTimeString(),
424
+      isHtml: true // 标记这是HTML内容
425
+    }
426
+    chatMessages.value.push(uploadMessage);
427
+    ElMessage.success('文件上传成功');
428
+    await addTopic({ agentId: props.agentId, topic: fileName })
429
+    const res = await listTopic({ agentId: props.agentId })
430
+    if (res.rows.length > 0) {
431
+      const topicId = res.rows[0].topicId
432
+      await addChat({ userId: userStore.id, chatId, topicId, output: assistantContent, outputTime: proxy.parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') })
433
+    }
434
+
435
+    nextTick(() => {
436
+      scrollToBottom()
437
+    })
438
+  } catch (error) {
439
+    console.error('文件上传失败:', error)
440
+    ElMessage.error('文件上传失败')
441
+
442
+    // 添加上传失败的消息到聊天记录
443
+    const errorMessage = {
444
+      role: 'assistant',
445
+      content: '文件上传失败,请检查文件格式或网络连接后重试。',
446
+      timestamp: new Date().toLocaleTimeString(),
447
+      isHtml: false
448
+    }
449
+    chatMessages.value.push(errorMessage)
450
+
451
+    nextTick(() => {
452
+      scrollToBottom()
453
+    })
454
+  }
455
+}
456
+
457
+// 清空聊天内文件列表
458
+const clearChatFiles = () => {
459
+  chatFileList.value = []
460
+}
461
+
462
+// 开始新对话
463
+const startNewChat = () => {
464
+  currentTopicId.value = null
465
+  chatMessages.value = []
466
+  chatFileList.value = []
467
+  chatTitle.value = '智能体新对话'
468
+
469
+  // 滚动到顶部显示开场白
470
+  nextTick(() => {
471
+    if (messagesContainer.value) {
472
+      messagesContainer.value.scrollTop = 0
473
+    }
474
+  })
475
+
476
+  ElMessage.success('已切换到新对话模式')
477
+}
478
+
479
+// 清空对话
480
+const clearChat = () => {
481
+  chatMessages.value = []
482
+  chatFileList.value = []
483
+  chatTitle.value = '智能体新对话'
484
+
485
+  // 滚动到顶部显示开场白
486
+  nextTick(() => {
487
+    if (messagesContainer.value) {
488
+      messagesContainer.value.scrollTop = 0
489
+    }
490
+  })
491
+
492
+  ElMessage.success('对话已清空')
493
+}
494
+
495
+// 格式化内容中的链接
496
+const formatContentLinks = (content) => {
497
+  if (!content) return content
498
+
499
+  // 使用正则表达式匹配 <a href='/profile/...'>...</a> 格式的链接
500
+  const linkRegex = /<a\s+href=['"]([^'"]*?)['"][^>]*?>(.*?)<\/a>/gi
501
+
502
+  let formattedContent = content.replace(linkRegex, (match, href, text) => {
503
+    // 如果href不是以http开头的,说明是相对路径,需要添加基础API地址
504
+    if (!href.startsWith('http')) {
505
+      const baseApi = import.meta.env.VITE_APP_BASE_API || ''
506
+      const fullUrl = baseApi + href
507
+      return `<a href="${fullUrl}" target="_blank" style="color: #1890ff; text-decoration: underline;">${text}</a>`
508
+    }
509
+    return match
510
+  })
511
+
512
+  // 将普通换行符转换为HTML换行符
513
+  formattedContent = formattedContent.replace(/\n/g, '<br>')
514
+
515
+  return formattedContent
516
+}
517
+</script>
518
+
519
+<style lang="scss" scoped>
520
+.agent-detail-container {
521
+  height: 100%;
522
+  display: flex;
523
+  flex-direction: column;
524
+}
525
+
526
+.detail-content {
527
+  flex: 1;
528
+  display: flex;
529
+  flex-direction: column;
530
+  overflow: hidden;
531
+}
532
+
533
+.agent-header {
534
+  padding: 15px;
535
+  border-bottom: 1px solid #e4e4e4;
536
+  background: white;
537
+  display: flex;
538
+  justify-content: space-between;
539
+  align-items: flex-start;
540
+
541
+  .agent-basic-info {
542
+    flex: 1;
543
+
544
+    .agent-title {
545
+      margin: 0 0 8px 0;
546
+      font-size: 24px;
547
+      font-weight: 600;
548
+      color: #333;
549
+    }
550
+
551
+    .agent-description {
552
+      margin: 0 0 12px 0;
553
+      font-size: 14px;
554
+      color: #666;
555
+      line-height: 1.5;
556
+    }
557
+
558
+    .agent-meta-info {
559
+      display: flex;
560
+      gap: 16px;
561
+
562
+      .meta-item {
563
+        display: flex;
564
+        align-items: center;
565
+        gap: 4px;
566
+        font-size: 12px;
567
+        color: #999;
568
+
569
+        .el-icon {
570
+          font-size: 14px;
571
+        }
572
+      }
573
+    }
574
+  }
575
+
576
+  .agent-actions {
577
+    display: flex;
578
+    gap: 12px;
579
+  }
580
+}
581
+
582
+.chat-section {
583
+  background: white;
584
+  margin: 16px 24px;
585
+  border-radius: 8px;
586
+  border: 1px solid #e4e4e4;
587
+  overflow: hidden;
588
+  display: flex;
589
+  height: calc(100vh - 200px);
590
+
591
+  // 话题列表样式
592
+  .topic-list {
593
+    width: 250px;
594
+    border-right: 1px solid #e4e4e4;
595
+    display: flex;
596
+    flex-direction: column;
597
+    background: #f8f9fa;
598
+
599
+    .topic-header {
600
+      padding: 16px 20px;
601
+      border-bottom: 1px solid #e4e4e4;
602
+      background: #f5f7fa;
603
+      color: #495057;
604
+
605
+      h4 {
606
+        margin: 0;
607
+        font-size: 14px;
608
+        font-weight: 600;
609
+        color: #495057;
610
+        display: flex;
611
+        align-items: center;
612
+
613
+        &::before {
614
+          content: "📚";
615
+          margin-right: 8px;
616
+          font-size: 16px;
617
+        }
618
+      }
619
+    }
620
+
621
+    .topic-content {
622
+      flex: 1;
623
+      overflow-y: auto;
624
+      padding: 8px 0;
625
+
626
+      .topic-item {
627
+        padding: 12px 16px;
628
+        border-bottom: 1px solid #e9ecef;
629
+        cursor: pointer;
630
+        transition: all 0.3s ease;
631
+        background: white;
632
+        margin: 4px 8px;
633
+        border-radius: 6px;
634
+        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
635
+
636
+        &:hover {
637
+          background: #e3f2fd;
638
+          transform: translateX(2px);
639
+          box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
640
+        }
641
+
642
+        &.active {
643
+          background: #f5f7fa;
644
+          color: #495057;
645
+          transform: translateX(3px);
646
+          box-shadow: 0 3px 12px rgba(73, 80, 87, 0.2);
647
+          border-left: 3px solid #409EFF;
648
+
649
+          .topic-info {
650
+            .topic-name {
651
+              color: #495057;
652
+              font-weight: 600;
653
+
654
+              &::before {
655
+                content: "💬";
656
+                filter: none;
657
+              }
658
+            }
659
+
660
+            .topic-time {
661
+              color: #6c757d;
662
+            }
663
+          }
664
+        }
665
+
666
+        &:last-child {
667
+          border-bottom: none;
668
+        }
669
+
670
+        .topic-info {
671
+          .topic-name {
672
+            font-size: 13px;
673
+            font-weight: 500;
674
+            color: #495057;
675
+            margin-bottom: 4px;
676
+            overflow: hidden;
677
+            text-overflow: ellipsis;
678
+            white-space: nowrap;
679
+
680
+            &::before {
681
+              content: "💬";
682
+              margin-right: 6px;
683
+              font-size: 12px;
684
+            }
685
+          }
686
+
687
+          .topic-time {
688
+            font-size: 11px;
689
+            color: #6c757d;
690
+            font-style: italic;
691
+          }
692
+        }
693
+      }
694
+    }
695
+  }
696
+
697
+  // 对话内容区域样式
698
+  .chat-content {
699
+    flex: 1;
700
+    display: flex;
701
+    flex-direction: column;
702
+    background: #fdfdfd;
703
+
704
+    .section-header {
705
+      padding: 16px 20px;
706
+      border-bottom: 1px solid #e4e4e4;
707
+      background: #f5f7fa;
708
+      display: flex;
709
+      justify-content: space-between;
710
+      align-items: center;
711
+
712
+      h3 {
713
+        margin: 0;
714
+        font-size: 16px;
715
+        font-weight: 600;
716
+        color: #495057;
717
+        display: flex;
718
+        align-items: center;
719
+
720
+        &::before {
721
+          content: "✨";
722
+          margin-right: 8px;
723
+          font-size: 18px;
724
+        }
725
+      }
726
+
727
+      .header-actions {
728
+        display: flex;
729
+        gap: 8px;
730
+      }
731
+    }
732
+
733
+    .chat-messages {
734
+      flex: 1;
735
+      padding: 16px 20px;
736
+      overflow-y: auto;
737
+      max-height: 475px;
738
+
739
+      .message-item {
740
+        display: flex;
741
+        margin-bottom: 16px;
742
+        align-items: flex-start;
743
+
744
+        &.user-message {
745
+          flex-direction: row-reverse;
746
+
747
+          .message-avatar {
748
+            background: #1890ff;
749
+            color: white;
750
+            margin-left: 12px;
751
+            margin-right: 0;
752
+          }
753
+
754
+          .message-content {
755
+            text-align: right;
756
+
757
+            .message-text {
758
+              background: #1890ff;
759
+              color: white;
760
+            }
761
+          }
762
+        }
763
+
764
+        &.assistant-message {
765
+          .message-avatar {
766
+            background: #f0f0f0;
767
+            color: #666;
768
+            margin-right: 12px;
769
+          }
770
+
771
+          .message-content {
772
+            .message-text {
773
+              background: #f0f0f0;
774
+              color: #333;
775
+            }
776
+          }
777
+
778
+          // 文件信息消息特殊样式
779
+          &.file-info-message {
780
+            .message-avatar {
781
+              background: #e3f2fd;
782
+              color: #1976d2;
783
+            }
784
+
785
+            .message-content {
786
+              .message-text {
787
+                background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
788
+                color: #1565c0;
789
+                border-left: 4px solid #2196f3;
790
+                font-weight: 500;
791
+
792
+                &::before {
793
+                  content: "📋";
794
+                  margin-right: 8px;
795
+                  font-size: 16px;
796
+                }
797
+              }
798
+            }
799
+          }
800
+        }
801
+
802
+        .message-avatar {
803
+          width: 32px;
804
+          height: 32px;
805
+          border-radius: 50%;
806
+          display: flex;
807
+          align-items: center;
808
+          justify-content: center;
809
+          font-size: 30px;
810
+          flex-shrink: 0;
811
+        }
812
+
813
+        .message-content {
814
+          max-width: 70%;
815
+
816
+          .message-text {
817
+            padding: 8px 12px;
818
+            border-radius: 8px;
819
+            font-size: 14px;
820
+            line-height: 1.5;
821
+            word-wrap: break-word;
822
+
823
+            // HTML消息中的链接样式
824
+            :deep(a) {
825
+              color: #1890ff;
826
+              text-decoration: underline;
827
+
828
+              &:hover {
829
+                color: #40a9ff;
830
+                text-decoration: none;
831
+              }
832
+            }
833
+
834
+            // 换行处理
835
+            :deep(br) {
836
+              display: block;
837
+              margin: 4px 0;
838
+            }
839
+          }
840
+
841
+          .message-time {
842
+            font-size: 11px;
843
+            color: #999;
844
+            margin-top: 4px;
845
+          }
846
+        }
847
+
848
+        // 聊天内文件上传区域样式
849
+        .message-upload-area {
850
+          background: #f8f9fa;
851
+          border: 1px dashed #d0d7de;
852
+          border-radius: 8px;
853
+          padding: 16px;
854
+
855
+          .upload-tip {
856
+            margin: 0 0 12px 0;
857
+            font-size: 14px;
858
+            color: #333;
859
+          }
860
+
861
+          .inline-upload {
862
+            margin-bottom: 12px;
863
+
864
+            :deep(.el-upload-list) {
865
+              margin-top: 8px;
866
+
867
+              .el-upload-list__item {
868
+                font-size: 12px;
869
+                margin: 2px 0;
870
+              }
871
+            }
872
+          }
873
+
874
+          .chat-upload-actions {
875
+            display: flex;
876
+            gap: 8px;
877
+            margin-top: 8px;
878
+          }
879
+        }
880
+      }
881
+    }
882
+
883
+    .chat-input-area {
884
+      padding: 16px 20px;
885
+      border-top: 1px solid #e4e4e4;
886
+
887
+      .input-actions {
888
+        display: flex;
889
+        justify-content: space-between;
890
+        align-items: center;
891
+        margin-top: 8px;
892
+
893
+        .input-tip {
894
+          font-size: 12px;
895
+          color: #999;
896
+        }
897
+      }
898
+    }
899
+  }
900
+}
901
+
902
+// 打字指示器动画
903
+.typing-indicator {
904
+  display: flex;
905
+  gap: 4px;
906
+  padding: 8px 12px;
907
+  background: #f0f0f0;
908
+  border-radius: 8px;
909
+
910
+  span {
911
+    width: 6px;
912
+    height: 6px;
913
+    background: #999;
914
+    border-radius: 50%;
915
+    animation: typing 1.4s infinite ease-in-out;
916
+
917
+    &:nth-child(1) {
918
+      animation-delay: -0.32s;
919
+    }
920
+
921
+    &:nth-child(2) {
922
+      animation-delay: -0.16s;
923
+    }
924
+  }
925
+}
926
+
927
+@keyframes typing {
928
+
929
+  0%,
930
+  80%,
931
+  100% {
932
+    transform: scale(0.8);
933
+    opacity: 0.6;
934
+  }
935
+
936
+  40% {
937
+    transform: scale(1);
938
+    opacity: 1;
939
+  }
940
+}
941
+
942
+.empty-state {
943
+  flex: 1;
944
+  display: flex;
945
+  flex-direction: column;
946
+  align-items: center;
947
+  justify-content: center;
948
+  color: #999;
949
+
950
+  p {
951
+    margin-top: 16px;
952
+    font-size: 14px;
953
+  }
954
+}
955
+</style>

+ 4
- 45
llm-ui/src/views/llm/agent/index.vue Bestand weergeven

@@ -1,8 +1,8 @@
1 1
 <!--
2 2
  * @Author: ysh
3 3
  * @Date: 2025-07-17 18:16:50
4
- * @LastEditors: wrh
5
- * @LastEditTime: 2025-07-28 09:47:19
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-28 14:46:01
6 6
 -->
7 7
 <template>
8 8
   <div class="agent-container">
@@ -76,22 +76,7 @@
76 76
 
77 77
     <!-- 右侧详细内容 -->
78 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>
79
+      <AgentDetail :agent-id="selectedAgentId" />
95 80
     </div>
96 81
 
97 82
     <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑智能体' : '新增智能体'" width="30%" @close="resetForm">
@@ -116,6 +101,7 @@ import { ref, reactive, onMounted } from 'vue'
116 101
 import { ElMessage, ElMessageBox } from 'element-plus'
117 102
 import { listAgent, addAgent, updateAgent, delAgent, opening } from '@/api/llm/agent'
118 103
 import { answer } from '@/api/llm/mcp'
104
+import AgentDetail from './AgentDetail.vue'
119 105
 
120 106
 // 响应式数据
121 107
 const loading = ref(false)
@@ -165,10 +151,6 @@ const handleQuery = () => {
165 151
 // 选择智能体
166 152
 const selectAgent = (agent) => {
167 153
   selectedAgentId.value = agent.agentId
168
-  console.log('选中智能体:', agent)
169
-  opening(agent.agentName).then(res => {
170
-    console.log('开场白:', res.resultContent)
171
-  })
172 154
 }
173 155
 
174 156
 const resetForm = () => {
@@ -366,29 +348,6 @@ onMounted(() => {
366 348
 .agent-detail {
367 349
   flex: 1;
368 350
   background: white;
369
-  display: flex;
370
-  align-items: center;
371
-  justify-content: center;
372
-
373
-  .detail-placeholder {
374
-    text-align: center;
375
-    color: #999;
376
-
377
-    .coming-soon {
378
-      .sub-text {
379
-        font-size: 12px;
380
-        margin-top: 8px;
381
-        color: #ccc;
382
-      }
383
-    }
384
-
385
-    .no-selection {
386
-      p {
387
-        margin-top: 16px;
388
-        font-size: 14px;
389
-      }
390
-    }
391
-  }
392 351
 }
393 352
 
394 353
 :deep(.el-pagination) {

+ 5
- 3
llm-ui/src/views/llm/chat/index.vue Bestand weergeven

@@ -1,8 +1,8 @@
1 1
 <!--
2 2
  * @Author: ysh
3 3
  * @Date: 2025-04-07 14:14:05
4
- * @LastEditors: wrh
5
- * @LastEditTime: 2025-07-22 17:44:45
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2025-07-29 15:25:17
6 6
 -->
7 7
 <template>
8 8
   <div class="app-container">
@@ -385,7 +385,8 @@ const createNewChat = async () => {
385 385
   }
386 386
   try {
387 387
     const newTopic = {
388
-      topic: '新对话'
388
+      topic: '新对话',
389
+      agentId: 0,
389 390
     };
390 391
     const response = await addTopic(newTopic);
391 392
     await getList();
@@ -642,6 +643,7 @@ const data = reactive({
642 643
     pageNum: 1,
643 644
     pageSize: 99999,
644 645
     topic: null,
646
+    agentId: 0,
645 647
   },
646 648
   rules: {
647 649
   }

Laden…
Annuleren
Opslaan