feat: 增加工具调用结果组件
- 在 AgentMessageComponent.vue 中优化工具参数和结果展示。 - 新增 CalculatorResult、KnowledgeBaseResult、KnowledgeGraphResult、WebSearchResult 和 ToolResultRenderer 组件,支持不同类型的工具调用结果展示。 - 添加工具调用结果的样式和逻辑处理。
This commit is contained in:
parent
4ecf9a8cd6
commit
d1f4d850f5
@ -43,18 +43,17 @@
|
||||
</div>
|
||||
<div class="tool-content" v-show="expandedToolCalls.has(toolCall.id)">
|
||||
<div class="tool-params" v-if="toolCall.args || toolCall.function.arguments">
|
||||
<div class="tool-params-header">
|
||||
参数:
|
||||
</div>
|
||||
<div class="tool-params-content">
|
||||
<pre>{{ toolCall.args || toolCall.function.arguments }}</pre>
|
||||
<pre> 参数: {{ toolCall.args || toolCall.function.arguments }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-params" v-if="toolCall.tool_call_result && toolCall.tool_call_result.content">
|
||||
<div class="tool-params-header">
|
||||
执行结果
|
||||
<div class="tool-result" v-if="toolCall.tool_call_result && toolCall.tool_call_result.content">
|
||||
<div class="tool-result-content">
|
||||
<ToolResultRenderer
|
||||
:tool-name="toolCall.name || toolCall.function.name"
|
||||
:result-content="toolCall.tool_call_result.content"
|
||||
/>
|
||||
</div>
|
||||
<div class="tool-params-content">{{ toolCall.tool_call_result.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -84,6 +83,7 @@
|
||||
import { computed, ref } from 'vue';
|
||||
import { CaretRightOutlined, ThunderboltOutlined, LoadingOutlined } from '@ant-design/icons-vue';
|
||||
import RefsComponent from '@/components/RefsComponent.vue'
|
||||
import { ToolResultRenderer } from '@/components/ToolCallingResult'
|
||||
|
||||
|
||||
import { MdPreview } from 'md-editor-v3'
|
||||
@ -309,6 +309,25 @@ const toggleToolCall = (toolCallId) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tool-result {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
|
||||
.tool-result-header {
|
||||
padding: 10px 12px;
|
||||
background-color: var(--gray-100);
|
||||
font-size: 12px;
|
||||
color: var(--gray-700);
|
||||
font-weight: 500;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
}
|
||||
|
||||
.tool-result-content {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
|
||||
212
web/src/components/ToolCallingResult/CalculatorResult.vue
Normal file
212
web/src/components/ToolCallingResult/CalculatorResult.vue
Normal file
@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<div class="calculator-result">
|
||||
<div class="calc-header">
|
||||
<h4><NumberOutlined /> 计算结果</h4>
|
||||
</div>
|
||||
|
||||
<div class="calc-display">
|
||||
<div class="result-container">
|
||||
<div class="result-label">结果:</div>
|
||||
<div class="result-value">{{ formatNumber(data) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="result-actions">
|
||||
<a-button size="small" @click="copyResult" type="primary" ghost>
|
||||
<CopyOutlined /> 复制结果
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 如果结果很大或很小,显示科学记数法 -->
|
||||
<div v-if="showScientificNotation" class="scientific-notation">
|
||||
<span class="notation-label">科学记数法:</span>
|
||||
<span class="notation-value">{{ data.toExponential(6) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 显示数字类型信息 -->
|
||||
<div class="number-info">
|
||||
<a-tag :color="getNumberTypeColor()">{{ getNumberType() }}</a-tag>
|
||||
<span v-if="isInteger" class="number-detail">整数</span>
|
||||
<span v-else class="number-detail">{{ getDecimalPlaces() }} 位小数</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { NumberOutlined, CopyOutlined } from '@ant-design/icons-vue'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const { copy } = useClipboard()
|
||||
|
||||
// 计算属性
|
||||
const isInteger = computed(() => Number.isInteger(props.data))
|
||||
|
||||
const showScientificNotation = computed(() => {
|
||||
const absValue = Math.abs(props.data)
|
||||
return absValue >= 1e6 || (absValue > 0 && absValue < 1e-3)
|
||||
})
|
||||
|
||||
// 方法
|
||||
const formatNumber = (num) => {
|
||||
if (typeof num !== 'number') return String(num)
|
||||
|
||||
// 处理特殊值
|
||||
if (!isFinite(num)) {
|
||||
if (num === Infinity) return '∞'
|
||||
if (num === -Infinity) return '-∞'
|
||||
if (isNaN(num)) return 'NaN'
|
||||
}
|
||||
|
||||
// 使用本地化格式
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
maximumFractionDigits: 10,
|
||||
useGrouping: true
|
||||
}).format(num)
|
||||
}
|
||||
|
||||
const getNumberType = () => {
|
||||
if (!isFinite(props.data)) {
|
||||
if (isNaN(props.data)) return 'NaN'
|
||||
return '无穷大'
|
||||
}
|
||||
|
||||
if (isInteger.value) {
|
||||
if (props.data === 0) return '零'
|
||||
if (props.data > 0) return '正整数'
|
||||
return '负整数'
|
||||
}
|
||||
|
||||
if (props.data > 0) return '正数'
|
||||
if (props.data < 0) return '负数'
|
||||
return '数字'
|
||||
}
|
||||
|
||||
const getNumberTypeColor = () => {
|
||||
if (!isFinite(props.data)) return 'red'
|
||||
if (props.data === 0) return 'default'
|
||||
if (props.data > 0) return 'green'
|
||||
return 'orange'
|
||||
}
|
||||
|
||||
const getDecimalPlaces = () => {
|
||||
if (isInteger.value) return 0
|
||||
const decimalPart = String(props.data).split('.')[1]
|
||||
return decimalPart ? decimalPart.length : 0
|
||||
}
|
||||
|
||||
const copyResult = async () => {
|
||||
try {
|
||||
await copy(String(props.data))
|
||||
message.success('计算结果已复制到剪贴板')
|
||||
} catch (error) {
|
||||
message.error('复制失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.calculator-result {
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
.calc-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: var(--main-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.calc-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 8px;
|
||||
padding: 16px;
|
||||
background: var(--main-10);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--main-200);
|
||||
|
||||
.result-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
|
||||
.result-label {
|
||||
font-size: 13px;
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: var(--main-color);
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.scientific-notation {
|
||||
margin: 0 8px 8px 8px;
|
||||
padding: 8px 10px;
|
||||
background: var(--gray-50);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.notation-label {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
|
||||
.notation-value {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--main-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.number-info {
|
||||
padding: 0 8px 8px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
.number-detail {
|
||||
color: var(--gray-600);
|
||||
background: var(--gray-100);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
232
web/src/components/ToolCallingResult/KnowledgeBaseResult.vue
Normal file
232
web/src/components/ToolCallingResult/KnowledgeBaseResult.vue
Normal file
@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<div class="knowledge-base-result">
|
||||
<div class="kb-header">
|
||||
<h4><FileTextOutlined /> 知识库检索结果</h4>
|
||||
<div class="result-summary">
|
||||
找到 {{ data.length }} 个相关文档片段
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kb-results">
|
||||
<div
|
||||
v-for="(result, index) in data"
|
||||
:key="result.id"
|
||||
class="kb-result-item"
|
||||
:class="{ 'high-relevance': result.rerank_score > 0.5 }"
|
||||
>
|
||||
<div class="result-header">
|
||||
<div class="result-index">#{{ index + 1 }}</div>
|
||||
<div class="result-file">
|
||||
<FileOutlined />
|
||||
{{ result.file.filename }}
|
||||
</div>
|
||||
<div class="result-id">ID: {{ result.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="result-scores">
|
||||
<div class="score-item">
|
||||
<span class="score-label">相似度:</span>
|
||||
<a-progress
|
||||
:percent="getPercent(result.distance)"
|
||||
size="small"
|
||||
:stroke-color="getScoreColor(result.distance)"
|
||||
/>
|
||||
<span class="score-value">{{ result.distance.toFixed(4) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="result.rerank_score" class="score-item">
|
||||
<span class="score-label">重排序:</span>
|
||||
<a-progress
|
||||
:percent="getPercent(result.rerank_score)"
|
||||
size="small"
|
||||
:stroke-color="getScoreColor(result.rerank_score)"
|
||||
/>
|
||||
<span class="score-value">{{ result.rerank_score.toFixed(4) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-content">
|
||||
<div class="content-text">{{ result.entity.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="data.length === 0" class="no-results">
|
||||
<p>未找到相关知识库内容</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FileTextOutlined, FileOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const getPercent = (score) => {
|
||||
// 相似度分数通常在0-1之间,需要转换为百分比
|
||||
if (score <= 1) {
|
||||
return Math.round(score * 100)
|
||||
}
|
||||
// 如果分数大于1,可能需要不同的处理
|
||||
return Math.min(Math.round(score * 100), 100)
|
||||
}
|
||||
|
||||
const getScoreColor = (score) => {
|
||||
if (score >= 0.7) return '#52c41a' // 绿色 - 高相关性
|
||||
if (score >= 0.5) return '#faad14' // 橙色 - 中等相关性
|
||||
return '#ff4d4f' // 红色 - 低相关性
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.knowledge-base-result {
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
.kb-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 4px 0;
|
||||
color: var(--main-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.result-summary {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.kb-results {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kb-result-item {
|
||||
background: var(--gray-0);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--gray-200);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&.high-relevance {
|
||||
border-color: var(--main-300);
|
||||
background: var(--main-10);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--main-200);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.result-index {
|
||||
background: var(--main-color);
|
||||
color: var(--gray-0);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
min-width: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-file {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--gray-800);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
.anticon {
|
||||
color: var(--main-color);
|
||||
}
|
||||
}
|
||||
|
||||
.result-id {
|
||||
font-size: 11px;
|
||||
color: var(--gray-500);
|
||||
background: var(--gray-100);
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-scores {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.score-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
|
||||
.score-label {
|
||||
font-size: 11px;
|
||||
color: var(--gray-600);
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
:deep(.ant-progress) {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 10px;
|
||||
color: var(--gray-500);
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-content {
|
||||
.content-text {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--gray-700);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
background: var(--gray-50);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 2px solid var(--main-color);
|
||||
max-height: 160px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
padding: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
272
web/src/components/ToolCallingResult/KnowledgeGraphResult.vue
Normal file
272
web/src/components/ToolCallingResult/KnowledgeGraphResult.vue
Normal file
@ -0,0 +1,272 @@
|
||||
<template>
|
||||
<div class="knowledge-graph-result">
|
||||
<div class="kg-header">
|
||||
<h4><DeploymentUnitOutlined /> 知识图谱查询结果</h4>
|
||||
<div class="result-summary">
|
||||
找到 {{ totalNodes }} 个节点, {{ totalRelations }} 个关系
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图谱可视化容器 -->
|
||||
<div class="graph-visualization">
|
||||
<GraphContainer :graph-data="graphData" ref="graphContainer" />
|
||||
</div>
|
||||
|
||||
<!-- 详细信息展示 -->
|
||||
<div class="kg-details">
|
||||
<a-collapse v-model:activeKey="activeKeys" ghost>
|
||||
<a-collapse-panel key="entities" header="实体节点">
|
||||
<div class="entities-list">
|
||||
<a-tag
|
||||
v-for="entity in uniqueEntities"
|
||||
:key="entity.name"
|
||||
color="blue"
|
||||
class="entity-tag"
|
||||
>
|
||||
{{ entity.name }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
|
||||
<a-collapse-panel key="relations" header="关系详情">
|
||||
<div class="relations-list">
|
||||
<div
|
||||
v-for="(relation, index) in allRelations"
|
||||
:key="index"
|
||||
class="relation-item"
|
||||
>
|
||||
<div class="relation-content">
|
||||
<span class="entity-name">{{ relation.source }}</span>
|
||||
<span class="relation-type">{{ relation.type }}</span>
|
||||
<span class="entity-name">{{ relation.target }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
|
||||
<a-collapse-panel key="raw" header="原始数据">
|
||||
<pre class="raw-data">{{ JSON.stringify(data, null, 2) }}</pre>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { DeploymentUnitOutlined } from '@ant-design/icons-vue'
|
||||
import GraphContainer from '../GraphContainer.vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const activeKeys = ref(['entities'])
|
||||
const graphContainer = ref(null)
|
||||
|
||||
// 计算属性:解析图谱数据
|
||||
const graphData = computed(() => {
|
||||
const nodes = new Map()
|
||||
const edges = []
|
||||
let edgeId = 0
|
||||
|
||||
// 遍历所有路径数据
|
||||
props.data.forEach(pathData => {
|
||||
if (Array.isArray(pathData) && pathData.length >= 3) {
|
||||
const [startNode, relations, endNode] = pathData
|
||||
|
||||
// 添加起始节点
|
||||
if (startNode && startNode.properties && startNode.properties.name) {
|
||||
nodes.set(startNode.element_id, {
|
||||
id: startNode.element_id,
|
||||
name: startNode.properties.name
|
||||
})
|
||||
}
|
||||
|
||||
// 添加结束节点
|
||||
if (endNode && endNode.properties && endNode.properties.name) {
|
||||
nodes.set(endNode.element_id, {
|
||||
id: endNode.element_id,
|
||||
name: endNode.properties.name
|
||||
})
|
||||
}
|
||||
|
||||
// 添加关系
|
||||
if (Array.isArray(relations)) {
|
||||
relations.forEach(relation => {
|
||||
if (relation && relation.nodes && relation.type && relation.properties) {
|
||||
const [sourceNode, targetNode] = relation.nodes
|
||||
|
||||
// 确保源和目标节点存在
|
||||
if (sourceNode && sourceNode.properties && sourceNode.properties.name) {
|
||||
nodes.set(sourceNode.element_id, {
|
||||
id: sourceNode.element_id,
|
||||
name: sourceNode.properties.name
|
||||
})
|
||||
}
|
||||
|
||||
if (targetNode && targetNode.properties && targetNode.properties.name) {
|
||||
nodes.set(targetNode.element_id, {
|
||||
id: targetNode.element_id,
|
||||
name: targetNode.properties.name
|
||||
})
|
||||
}
|
||||
|
||||
// 添加边
|
||||
edges.push({
|
||||
source_id: sourceNode.element_id,
|
||||
target_id: targetNode.element_id,
|
||||
type: relation.properties.type || relation.type,
|
||||
id: `edge_${edgeId++}`
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
nodes: Array.from(nodes.values()),
|
||||
edges: edges
|
||||
}
|
||||
})
|
||||
|
||||
// 唯一实体列表
|
||||
const uniqueEntities = computed(() => {
|
||||
return graphData.value.nodes
|
||||
})
|
||||
|
||||
// 所有关系列表
|
||||
const allRelations = computed(() => {
|
||||
return graphData.value.edges.map(edge => {
|
||||
const sourceNode = graphData.value.nodes.find(n => n.id === edge.source_id)
|
||||
const targetNode = graphData.value.nodes.find(n => n.id === edge.target_id)
|
||||
return {
|
||||
source: sourceNode?.name || edge.source_id,
|
||||
target: targetNode?.name || edge.target_id,
|
||||
type: edge.type
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 统计信息
|
||||
const totalNodes = computed(() => graphData.value.nodes.length)
|
||||
const totalRelations = computed(() => graphData.value.edges.length)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.knowledge-graph-result {
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
.kg-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 4px 0;
|
||||
color: var(--main-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.result-summary {
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
}
|
||||
}
|
||||
|
||||
.graph-visualization {
|
||||
margin: 8px;
|
||||
background: var(--gray-0);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--gray-200);
|
||||
min-height: 350px;
|
||||
}
|
||||
|
||||
.kg-details {
|
||||
margin: 8px;
|
||||
background: var(--gray-0);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
:deep(.ant-collapse-header) {
|
||||
background: var(--gray-50) !important;
|
||||
border-radius: 4px !important;
|
||||
margin-bottom: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.entities-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
padding: 6px 0;
|
||||
|
||||
.entity-tag {
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.relations-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
.relation-item {
|
||||
padding: 8px 10px;
|
||||
background: var(--gray-50);
|
||||
border-radius: 4px;
|
||||
border-left: 2px solid var(--main-color);
|
||||
|
||||
.relation-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.entity-name {
|
||||
font-weight: 500;
|
||||
color: var(--main-color);
|
||||
background: var(--main-50);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.relation-type {
|
||||
color: var(--main-600);
|
||||
font-weight: 500;
|
||||
background: var(--gray-100);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--gray-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.raw-data {
|
||||
background: var(--gray-50);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
color: var(--gray-700);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
198
web/src/components/ToolCallingResult/ToolResultRenderer.vue
Normal file
198
web/src/components/ToolCallingResult/ToolResultRenderer.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="tool-result-renderer">
|
||||
<!-- 网页搜索结果 -->
|
||||
<WebSearchResult
|
||||
v-if="isWebSearchResult"
|
||||
:data="parsedData"
|
||||
/>
|
||||
|
||||
<!-- 知识库检索结果 -->
|
||||
<KnowledgeBaseResult
|
||||
v-else-if="isKnowledgeBaseResult"
|
||||
:data="parsedData"
|
||||
/>
|
||||
|
||||
<!-- 知识图谱查询结果 -->
|
||||
<KnowledgeGraphResult
|
||||
v-else-if="isKnowledgeGraphResult"
|
||||
:data="parsedData"
|
||||
/>
|
||||
|
||||
<!-- 计算器结果 -->
|
||||
<CalculatorResult
|
||||
v-else-if="isCalculatorResult"
|
||||
:data="parsedData"
|
||||
/>
|
||||
|
||||
<!-- 默认的原始数据展示 -->
|
||||
<div v-else class="default-result">
|
||||
<div class="default-header">
|
||||
<h4><ToolOutlined /> {{ toolName }} 执行结果</h4>
|
||||
</div>
|
||||
<div class="default-content">
|
||||
<pre>{{ formatData(parsedData) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { ToolOutlined } from '@ant-design/icons-vue'
|
||||
import WebSearchResult from './WebSearchResult.vue'
|
||||
import KnowledgeBaseResult from './KnowledgeBaseResult.vue'
|
||||
import KnowledgeGraphResult from './KnowledgeGraphResult.vue'
|
||||
import CalculatorResult from './CalculatorResult.vue'
|
||||
|
||||
const props = defineProps({
|
||||
toolName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
resultContent: {
|
||||
type: [String, Object, Array, Number],
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 解析数据
|
||||
const parsedData = computed(() => {
|
||||
if (typeof props.resultContent === 'string') {
|
||||
try {
|
||||
return JSON.parse(props.resultContent)
|
||||
} catch {
|
||||
return props.resultContent
|
||||
}
|
||||
}
|
||||
return props.resultContent
|
||||
})
|
||||
|
||||
// 判断是否为网页搜索结果
|
||||
const isWebSearchResult = computed(() => {
|
||||
const toolNameLower = props.toolName.toLowerCase()
|
||||
const isWebSearchTool = toolNameLower.includes('search') ||
|
||||
toolNameLower.includes('tavily') ||
|
||||
toolNameLower.includes('web')
|
||||
|
||||
if (!isWebSearchTool) return false
|
||||
|
||||
const data = parsedData.value
|
||||
return data &&
|
||||
typeof data === 'object' &&
|
||||
'results' in data &&
|
||||
Array.isArray(data.results) &&
|
||||
'query' in data
|
||||
})
|
||||
|
||||
// 判断是否为知识库检索结果
|
||||
const isKnowledgeBaseResult = computed(() => {
|
||||
const toolNameLower = props.toolName.toLowerCase()
|
||||
const isKnowledgeBaseTool = toolNameLower.includes('retrieve') ||
|
||||
toolNameLower.includes('knowledge')
|
||||
|
||||
if (!isKnowledgeBaseTool) return false
|
||||
|
||||
const data = parsedData.value
|
||||
return Array.isArray(data) &&
|
||||
data.length > 0 &&
|
||||
data.every(item =>
|
||||
item &&
|
||||
typeof item === 'object' &&
|
||||
'id' in item &&
|
||||
'distance' in item &&
|
||||
'entity' in item &&
|
||||
'file' in item
|
||||
)
|
||||
})
|
||||
|
||||
// 判断是否为知识图谱查询结果
|
||||
const isKnowledgeGraphResult = computed(() => {
|
||||
const toolNameLower = props.toolName.toLowerCase()
|
||||
const isGraphTool = toolNameLower.includes('graph') ||
|
||||
toolNameLower.includes('kg') ||
|
||||
toolNameLower.includes('query_knowledge_graph')
|
||||
|
||||
if (!isGraphTool) return false
|
||||
|
||||
const data = parsedData.value
|
||||
return Array.isArray(data) &&
|
||||
data.length > 0 &&
|
||||
data.some(item =>
|
||||
Array.isArray(item) &&
|
||||
item.length >= 3 &&
|
||||
item.some(subItem =>
|
||||
subItem &&
|
||||
typeof subItem === 'object' &&
|
||||
('element_id' in subItem || 'properties' in subItem)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
// 判断是否为计算器结果
|
||||
const isCalculatorResult = computed(() => {
|
||||
const toolNameLower = props.toolName.toLowerCase()
|
||||
const isCalculatorTool = toolNameLower.includes('calculator') ||
|
||||
toolNameLower.includes('calc') ||
|
||||
toolNameLower.includes('math')
|
||||
|
||||
if (!isCalculatorTool) return false
|
||||
|
||||
return typeof parsedData.value === 'number'
|
||||
})
|
||||
|
||||
// 格式化数据用于默认展示
|
||||
const formatData = (data) => {
|
||||
if (typeof data === 'object') {
|
||||
return JSON.stringify(data, null, 2)
|
||||
}
|
||||
return String(data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tool-result-renderer {
|
||||
width: 100%;
|
||||
|
||||
.default-result {
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
.default-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
color: var(--main-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.default-content {
|
||||
background: var(--gray-0);
|
||||
padding: 12px;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--gray-700);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: var(--gray-50);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 2px solid var(--main-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
188
web/src/components/ToolCallingResult/WebSearchResult.vue
Normal file
188
web/src/components/ToolCallingResult/WebSearchResult.vue
Normal file
@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<div class="web-search-result">
|
||||
<div class="search-header">
|
||||
<h4><GlobalOutlined /> 网页搜索结果</h4>
|
||||
<div class="search-meta">
|
||||
<span class="query-text">查询: {{ data.query }}</span>
|
||||
<span class="response-time">响应时间: {{ data.response_time }}s</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-results">
|
||||
<div
|
||||
v-for="(result, index) in data.results"
|
||||
:key="index"
|
||||
class="search-result-item"
|
||||
>
|
||||
<div class="result-header">
|
||||
<h5 class="result-title">
|
||||
<a :href="result.url" target="_blank" rel="noopener noreferrer">
|
||||
{{ result.title }}
|
||||
</a>
|
||||
</h5>
|
||||
<span class="result-score">相关度: {{ (result.score * 100).toFixed(1) }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="result-meta">
|
||||
<!-- <span class="result-url">{{ result.url }}</span> -->
|
||||
<span v-if="result.published_date" class="result-date">
|
||||
{{ formatDate(result.published_date) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="result-content">
|
||||
{{ result.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="data.results.length === 0" class="no-results">
|
||||
<p>未找到相关搜索结果</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { GlobalOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.web-search-result {
|
||||
background: var(--gray-0);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-200);
|
||||
|
||||
.search-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--gray-200);
|
||||
background: var(--gray-50);
|
||||
|
||||
h4 {
|
||||
margin: 0 0 6px 0;
|
||||
color: var(--main-color);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.search-meta {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 12px;
|
||||
color: var(--gray-600);
|
||||
|
||||
.query-text {
|
||||
font-weight: 500;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-results {
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
background: var(--gray-0);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--gray-200);
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--main-200);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.result-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.result-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
|
||||
a {
|
||||
color: var(--main-color);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
color: var(--main-600);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.result-score {
|
||||
font-size: 11px;
|
||||
color: var(--gray-600);
|
||||
background: var(--gray-100);
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--gray-500);
|
||||
|
||||
.result-url {
|
||||
color: var(--main-400);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.result-date {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
}
|
||||
|
||||
.result-content {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--gray-700);
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
color: var(--gray-500);
|
||||
padding: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
6
web/src/components/ToolCallingResult/index.js
Normal file
6
web/src/components/ToolCallingResult/index.js
Normal file
@ -0,0 +1,6 @@
|
||||
// 工具调用结果组件导出
|
||||
export { default as WebSearchResult } from './WebSearchResult.vue'
|
||||
export { default as KnowledgeBaseResult } from './KnowledgeBaseResult.vue'
|
||||
export { default as KnowledgeGraphResult } from './KnowledgeGraphResult.vue'
|
||||
export { default as CalculatorResult } from './CalculatorResult.vue'
|
||||
export { default as ToolResultRenderer } from './ToolResultRenderer.vue'
|
||||
Loading…
Reference in New Issue
Block a user