ForcePilot/web/src/components/ToolCallingResult/tools/KnowledgeBaseTool.vue
Wenjie Zhang b0fa9d249b fix: 修复工具数据类型转换导致的调用结果无法识别
修复agentStore.availableTools的处理逻辑,统一使用Object.values获取工具列表
将进度条的stroke-width属性改为动态绑定格式
2025-12-28 23:00:53 +08:00

456 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<BaseToolCall :tool-call="toolCall">
<template #result="{ resultContent }">
<div class="knowledge-base-result">
<div class="result-summary">
找到 {{ parsedData(resultContent).length }} 个相关文档片段来自 {{ fileGroups(parsedData(resultContent)).length }} 个文件
</div>
<div class="kb-results">
<div
v-for="fileGroup in fileGroups(parsedData(resultContent))"
:key="fileGroup.filename"
class="file-group"
>
<!-- 文件级别的头部 -->
<div
class="file-header"
:class="{ 'expanded': expandedFiles.has(fileGroup.filename) }"
@click="toggleFile(fileGroup.filename)"
>
<div class="file-info">
<FileOutlined />
<span class="file-name">{{ fileGroup.filename }}</span>
<span class="chunk-count">{{ fileGroup.chunks.length }} chunks</span>
</div>
<div class="expand-icon">
<DownOutlined :class="{ 'rotated': expandedFiles.has(fileGroup.filename) }" />
</div>
</div>
<!-- 展开的chunks列表 -->
<div
v-if="expandedFiles.has(fileGroup.filename)"
class="chunks-container"
>
<div
v-for="(chunk, index) in fileGroup.chunks"
:key="chunk.id"
class="chunk-item"
:class="{ 'high-relevance': chunk.score > 0.5 }"
@click="showChunkDetail(chunk, index + 1)"
>
<div class="chunk-summary">
<span class="chunk-index">#{{ index + 1 }}</span>
<div class="chunk-scores">
<span class="score-item">相似度 {{ (chunk.score * 100).toFixed(0) }}%</span>
<span v-if="chunk.rerank_score" class="score-item">重排序 {{ (chunk.rerank_score * 100).toFixed(0) }}%</span>
</div>
<span class="chunk-preview">{{ getPreviewText(chunk.content) }}</span>
<EyeOutlined class="view-icon" />
</div>
</div>
</div>
</div>
</div>
<div v-if="parsedData(resultContent).length === 0" class="no-results">
<p>未找到相关知识库内容</p>
</div>
<!-- 弹窗展示chunk详细信息 -->
<a-modal
v-model:open="modalVisible"
:title="`文档片段 #${selectedChunk?.index} - ${selectedChunk?.data?.metadata?.source}`"
width="800px"
:footer="null"
class="chunk-detail-modal"
>
<div v-if="selectedChunk" class="chunk-detail">
<div class="detail-header">
<div class="detail-scores">
<div class="score-card">
<div class="score-label">相似度分数</div>
<div class="score-value-large">{{ (selectedChunk.data.score * 100).toFixed(1) }}%</div>
<a-progress
:percent="getPercent(selectedChunk.data.score)"
:stroke-color="getScoreColor(selectedChunk.data.score)"
:show-info="false"
:stroke-width="6"
/>
</div>
<div v-if="selectedChunk.data.rerank_score" class="score-card">
<div class="score-label">重排序分数</div>
<div class="score-value-large">{{ (selectedChunk.data.rerank_score * 100).toFixed(1) }}%</div>
<a-progress
:percent="getPercent(selectedChunk.data.rerank_score)"
:stroke-color="getScoreColor(selectedChunk.data.rerank_score)"
:show-info="false"
:stroke-width="6"
/>
</div>
</div>
<div class="detail-meta">
<span class="meta-item"><DatabaseOutlined /> ID: {{ selectedChunk.data.metadata.chunk_id || selectedChunk.data.metadata.file_id }}</span>
</div>
</div>
<div class="detail-content">
<h5>文档内容</h5>
<div class="content-text">{{ selectedChunk.data.content }}</div>
</div>
</div>
</a-modal>
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import BaseToolCall from '../BaseToolCall.vue';
import { ref, computed } from 'vue'
import { FileTextOutlined, FileOutlined, DownOutlined, EyeOutlined, DatabaseOutlined } from '@ant-design/icons-vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const parseData = (content) => {
if (typeof content === 'string') {
try {
return JSON.parse(content);
} catch (error) {
return [];
}
}
return content || [];
};
const parsedData = (content) => parseData(content);
// 管理展开状态
const expandedFiles = ref(new Set())
// 弹窗状态
const modalVisible = ref(false)
const selectedChunk = ref(null)
// 按文件名聚合数据
const fileGroups = (data) => {
const groups = new Map()
data.forEach(item => {
const filename = item.metadata.source
if (!groups.has(filename)) {
groups.set(filename, {
filename,
chunks: []
})
}
groups.get(filename).chunks.push(item)
})
// 转换为数组并按文件名排序
return Array.from(groups.values()).sort((a, b) => a.filename.localeCompare(b.filename))
}
// 切换文件展开/折叠状态
const toggleFile = (filename) => {
if (expandedFiles.value.has(filename)) {
expandedFiles.value.delete(filename)
} else {
expandedFiles.value.add(filename)
}
}
// 显示chunk详细信息
const showChunkDetail = (chunk, index) => {
selectedChunk.value = {
data: chunk,
index: index
}
modalVisible.value = true
}
// 获取预览文本
const getPreviewText = (text) => {
if (text.length <= 100) return text
return text.substring(0, 100) + '...'
}
const getPercent = (score) => {
if (score <= 1) {
return Math.round(score * 100)
}
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);
.result-summary {
padding: 12px 16px;
background: var(--gray-25);
font-size: 12px;
color: var(--gray-500);
border-bottom: 1px solid var(--gray-100);
}
.kb-results {
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
.file-group {
border: 1px solid var(--gray-150);
border-radius: 8px;
background: var(--gray-0);
overflow: hidden;
.file-header {
padding: 8px 14px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
transition: all 0.2s ease;
background: var(--gray-10);
&:hover {
background: var(--gray-25);
}
&.expanded {
background: var(--gray-25);
border-bottom: 1px solid var(--gray-100);
}
.file-info {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
.anticon {
color: var(--gray-500);
font-size: 13px;
}
.file-name {
font-size: 13px;
font-weight: 500;
color: var(--gray-700);
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chunk-count {
font-size: 11px;
color: var(--gray-500);
white-space: nowrap;
margin-right: 4px;
}
}
.expand-icon {
color: var(--gray-400);
transition: transform 0.2s ease;
font-size: 12px;
.rotated {
transform: rotate(180deg);
}
}
}
.chunks-container {
background: var(--gray-0);
}
.chunk-item {
padding: 10px 14px;
border-bottom: 1px solid var(--gray-100);
cursor: pointer;
transition: all 0.2s ease;
&:last-child {
border-bottom: none;
}
&.high-relevance {
background: var(--main-5);
}
&:hover {
background: var(--gray-25);
.view-icon {
opacity: 1;
}
}
.chunk-summary {
display: flex;
align-items: center;
gap: 10px;
.chunk-index {
color: var(--gray-500);
font-size: 11px;
font-weight: 500;
min-width: 20px;
text-align: center;
background: var(--gray-25);
padding: 1px 4px;
border-radius: 4px;
}
.chunk-scores {
display: flex;
gap: 6px;
.score-item {
font-size: 11px;
color: var(--gray-600);
background: var(--gray-25);
padding: 1px 5px;
border-radius: 4px;
border: 1px solid var(--gray-100);
white-space: nowrap;
}
}
.chunk-preview {
flex: 1;
font-size: 12px;
color: var(--gray-600);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.view-icon {
color: var(--gray-400);
font-size: 12px;
opacity: 0.5;
transition: opacity 0.2s ease;
}
}
}
}
.no-results {
text-align: center;
color: var(--gray-500);
padding: 20px;
font-size: 12px;
}
}
:deep(.chunk-detail-modal) {
.ant-modal-header {
border-bottom: 1px solid var(--gray-200);
margin-bottom: 0;
}
.ant-modal-title {
color: var(--main-color);
font-weight: 500;
}
}
.chunk-detail {
.detail-header {
margin-bottom: 16px;
.detail-scores {
display: flex;
gap: 16px;
margin-bottom: 12px;
.score-card {
flex: 1;
padding: 12px 14px;
background: var(--gray-25);
border-radius: 8px;
border: 1px solid var(--gray-150);
.score-label {
font-size: 12px;
color: var(--gray-500);
margin-bottom: 6px;
font-weight: 500;
}
.score-value-large {
font-size: 18px;
font-weight: 600;
color: var(--gray-800);
margin-bottom: 8px;
}
}
}
.detail-meta {
display: flex;
gap: 12px;
.meta-item {
font-size: 11px;
color: var(--gray-400);
display: flex;
align-items: center;
gap: 4px;
.anticon {
color: var(--gray-500);
}
}
}
}
.detail-content {
h5 {
margin: 0 0 8px 0;
color: var(--gray-800);
font-size: 14px;
font-weight: 500;
}
.content-text {
font-size: 13px;
line-height: 1.6;
color: var(--gray-700);
white-space: pre-wrap;
word-break: break-word;
background: var(--gray-25);
padding: 16px;
border-radius: 8px;
border: 1px solid var(--gray-150);
max-height: 400px;
overflow-y: auto;
}
}
}
</style>