Bladeren bron

美化工具调用window

kagg886 3 maanden geleden
bovenliggende
commit
86c0e82325

+ 222 - 0
src/components/markdown/plugins/impl/ToolsLoadingCard.vue

@@ -0,0 +1,222 @@
+<template>
+  <el-card
+    class="tools-loading-card"
+    shadow="hover"
+    @click="showDialog = true"
+    :style="{ cursor: 'pointer' }"
+  >
+    <template #header>
+      <div class="card-header">
+        <span class="card-title">{{ toolData.toolName || 'MCP工具调用' }}</span>
+        <el-icon :class="{ 'is-loading': isLoading }">
+          <Loading v-if="isLoading" />
+          <Tools v-else />
+        </el-icon>
+      </div>
+    </template>
+
+    <div class="card-content">
+      <p class="tool-description">
+        {{ getToolDescription() }}
+      </p>
+    </div>
+  </el-card>
+
+  <!-- 详情对话框 -->
+  <el-dialog
+    v-model="showDialog"
+    :title="toolData.toolName || 'MCP工具调用详情'"
+    width="800px"
+    destroy-on-close
+  >
+    <el-tabs v-model="activeTab" type="border-card">
+      <el-tab-pane label="工具信息" name="info">
+        <div class="tab-content">
+          <el-descriptions :column="1" border>
+            <el-descriptions-item label="工具名称">
+              {{ toolData.toolName || '未知工具' }}
+            </el-descriptions-item>
+            <el-descriptions-item label="调用状态">
+              <el-tag :type="getStatusType()">
+                {{ getStatusText() }}
+              </el-tag>
+            </el-descriptions-item>
+          </el-descriptions>
+        </div>
+      </el-tab-pane>
+
+      <el-tab-pane label="传入参数" name="params">
+        <div class="tab-content">
+          <div v-if="toolData.params">
+            <pre class="json-content">{{ toolData.params }}</pre>
+          </div>
+          <el-empty v-else description="无传入参数" />
+        </div>
+      </el-tab-pane>
+
+      <el-tab-pane label="返回结果" name="result">
+        <div class="tab-content">
+          <div v-if="toolData.result">
+            <pre class="json-content">{{ toolData.result }}</pre>
+          </div>
+          <div v-else-if="isLoading">
+            <el-skeleton :rows="5" animated />
+          </div>
+          <el-empty v-else description="暂无返回结果" />
+        </div>
+      </el-tab-pane>
+    </el-tabs>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { Loading, Tools } from '@element-plus/icons-vue'
+
+type Props = {
+  data: string
+}
+
+interface ToolData {
+  toolName: string
+  params: string
+  result?: string
+}
+
+const props = defineProps<Props>()
+
+const showDialog = ref(false)
+const activeTab = ref('info')
+const toolData = ref<ToolData>({
+  toolName: '',
+  params: '',
+  result: undefined
+})
+
+// 计算是否为加载状态(2行数据表示正在加载)
+const isLoading = computed(() => {
+  return !toolData.value.result
+})
+
+// 解析传入的数据
+onMounted(() => {
+  try {
+    const decodedData = decodeURIComponent(props.data)
+    const lines = decodedData.trim().split('\n')
+
+    if (lines.length >= 2) {
+      toolData.value = {
+        toolName: lines[0] || '未知工具',
+        params: lines[1] || '无参数',
+        result: lines[2] || undefined // 第三行为结果,可能不存在
+      }
+    } else {
+      toolData.value = {
+        toolName: '数据格式错误',
+        params: '应为2-3行格式',
+        result: undefined
+      }
+    }
+  } catch (e) {
+    console.error('解析工具数据失败:', e)
+    toolData.value = {
+      toolName: '数据解析失败',
+      params: '解析错误',
+      result: undefined
+    }
+  }
+})
+
+// 获取工具描述
+const getToolDescription = () => {
+  if (isLoading.value) {
+    return '工具正在执行中...'
+  }
+  return '工具执行完成,点击查看详情'
+}
+
+// 获取状态类型
+const getStatusType = () => {
+  if (isLoading.value) return 'warning'
+  return 'success'
+}
+
+// 获取状态文本
+const getStatusText = () => {
+  if (isLoading.value) return '执行中'
+  return '执行完成'
+}
+</script>
+
+<style scoped>
+.tools-loading-card {
+  transition: all 0.3s ease;
+  border: 1px solid var(--el-border-color-light);
+}
+
+.tools-loading-card:hover {
+  border-color: var(--el-color-primary);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.card-title {
+  font-weight: 600;
+  color: var(--el-text-color-primary);
+}
+
+.el-icon {
+  color: var(--el-color-primary);
+  transition: all 0.3s ease;
+}
+
+.is-loading {
+  color: var(--el-color-warning);
+  animation: rotating 2s linear infinite;
+}
+
+@keyframes rotating {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.card-content {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.tool-description {
+  margin: 0;
+  color: var(--el-text-color-regular);
+  font-size: 14px;
+}
+
+.tab-content {
+  padding: 16px 0;
+}
+
+.json-content {
+  background-color: var(--el-fill-color-light);
+  border: 1px solid var(--el-border-color-light);
+  border-radius: 6px;
+  padding: 16px;
+  margin: 0;
+  font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+  font-size: 13px;
+  line-height: 1.5;
+  color: var(--el-text-color-primary);
+  white-space: pre-wrap;
+  word-break: break-all;
+  overflow-x: auto;
+}
+</style>

+ 62 - 0
src/components/markdown/plugins/tools-loading.ts

@@ -0,0 +1,62 @@
+import MarkdownIt from "markdown-it";
+import type {RenderRule} from "markdown-it/lib/renderer.mjs";
+import type Token from "markdown-it/lib/token.mjs";
+import {defineMarkdownPlugin} from "../type/markdown.ts";
+import {h} from "vue";
+import ToolsLoadingCard from "./impl/ToolsLoadingCard.vue";
+
+// 验证2-3行格式
+function isValidLineFormat(str: string): boolean {
+    const lines = str.trim().split('\n')
+    return lines.length === 2 || lines.length === 3
+}
+
+// 渲染tools-loading代码块
+const renderToolsLoading: RenderRule = (tokens: Token[], idx: number) => {
+    const token = tokens[idx]
+    const content = token.content.trim()
+
+    if (!content) {
+        return '<div style="padding: 16px;background-color: #fff5f5;border: 1px solid #fed7d7;border-radius: 6px;color: #c53030;margin: 16px 0;">工具调用数据不能为空</div>'
+    }
+
+    if (!isValidLineFormat(content)) {
+        return '<div style="padding: 16px;background-color: #fff5f5;border: 1px solid #fed7d7;border-radius: 6px;color: #c53030;margin: 16px 0;">工具调用数据格式错误,应为2-3行格式:工具名、参数、[结果]</div>'
+    }
+
+    // 生成完整HTML
+    return `<tools-loading-container style="width: 100%;margin: 16px 0; border-radius: 6px" data="${encodeURIComponent(content)}"></tools-loading-container>`
+}
+
+const ToolsLoadingPlugin = defineMarkdownPlugin({
+    tagName: 'tools-loading-container',
+    mdItPlugin: function (md: MarkdownIt) {
+        // 保存原始的fence渲染器
+        const defaultRender = md.renderer.rules.fence ?? function (tokens, idx, options, _env, renderer) {
+            return renderer.renderToken(tokens, idx, options)
+        }
+
+        // 重写fence渲染器
+        md.renderer.rules.fence = function (tokens, idx, options, env, renderer) {
+            const token = tokens[idx]
+            const info = token.info ? token.info.trim() : ''
+
+            // 检查是否是tools-loading代码块
+            if (info === 'tools-loading' && isValidLineFormat(token.content.trim())) {
+                return renderToolsLoading(tokens, idx, options, env, renderer)
+            }
+
+            // 其他代码块使用默认渲染器
+            return defaultRender(tokens, idx, options, env, renderer)
+        }
+    },
+    renderer: (node: {attribs: Record<string, string>}) => {
+        return h(ToolsLoadingCard, {
+                data: node.attribs.data,
+                style: 'width: 100%;margin: 16px 0; border-radius: 6px'
+            }
+        )
+    }
+})
+
+export default ToolsLoadingPlugin

+ 2 - 1
src/views/assistant/index.vue

@@ -4,11 +4,12 @@ import { Local } from '/@/utils/storage'
 import { User, ChatDotRound, Delete, Edit, Check, Close } from '@element-plus/icons-vue'
 import { MarkdownPlugin } from '/@/components/markdown/type/markdown'
 import EChartsPlugin from '/@/components/markdown/plugins/echarts'
+import ToolsLoadingPlugin from '/@/components/markdown/plugins/tools-loading'
 import Markdown from '/@/components/markdown/Markdown.vue'
 import assist from '/@/api/assist'
 import { ChatResponse, Message } from '/@/api/assist/type'
 
-const plugins: Array<MarkdownPlugin<any>> = [EChartsPlugin()]
+const plugins: Array<MarkdownPlugin<any>> = [EChartsPlugin(), ToolsLoadingPlugin()]
 
 //聊天管理接口
 // 消息列表