feat: 增加工具调用结果组件

- 在 AgentMessageComponent.vue 中优化工具参数和结果展示。
- 新增 CalculatorResult、KnowledgeBaseResult、KnowledgeGraphResult、WebSearchResult 和 ToolResultRenderer 组件,支持不同类型的工具调用结果展示。
- 添加工具调用结果的样式和逻辑处理。
This commit is contained in:
Wenjie Zhang 2025-06-22 23:08:40 +08:00
parent 4ecf9a8cd6
commit d1f4d850f5
7 changed files with 1135 additions and 8 deletions

View File

@ -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 {

View 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>

View 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>

View 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>

View 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>

View 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>

View 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'