ForcePilot/web/src/components/ToolCallingResult/tools/KbDocumentPreview.vue

177 lines
4.4 KiB
Vue

<template>
<div class="kb-document-preview">
<div class="document-summary">
<div class="summary-main">{{ summaryText }}</div>
<div v-if="result?.file_id" class="summary-meta">file_id: {{ result.file_id }}</div>
</div>
<div v-if="documentWindows.length > 0" class="window-list">
<div
v-for="(window, windowIndex) in documentWindows"
:key="windowKey(window, windowIndex)"
class="window-card"
>
<div class="window-title">
<span>{{ windowTitle(window, windowIndex) }}</span>
<span v-if="window.matched_lines?.length" class="matched-count">
命中 {{ window.matched_lines.length }} 行
</span>
</div>
<pre class="content-preview"><span
v-for="(line, lineIndex) in splitContent(window.content)"
:key="`${windowKey(window, windowIndex)}-${lineIndex}`"
class="content-line"
:class="{ matched: isMatchedLine(line, window.matched_lines) }"
>{{ line }}</span></pre>
</div>
</div>
<div v-else class="empty-text">暂无可预览内容</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
result: {
type: Object,
required: true
},
mode: {
type: String,
default: 'open'
}
})
const documentWindows = computed(() => {
if (Array.isArray(props.result.windows)) return props.result.windows
if (props.result.content) return [props.result]
return []
})
const summaryText = computed(() => {
if (props.mode === 'find') {
const modeText = props.result.match_mode === 'regex' ? '正则' : '关键词'
return `${modeText}查找: ${props.result.total_matches || 0} 处匹配,${documentWindows.value.length} 个上下文窗口`
}
const startLine = props.result.start_line || 0
const endLine = props.result.end_line || 0
const totalLines = props.result.total_lines || 0
const moreText = props.result.has_more_after ? `,下一段 offset ${props.result.next_offset}` : ''
return `打开文档: 第 ${startLine}-${endLine} 行 / 共 ${totalLines} 行${moreText}`
})
const splitContent = (content = '') => String(content).split('\n')
const lineNumber = (line) => {
const match = String(line).match(/^\s*(\d+)\t/)
return match ? Number(match[1]) : null
}
const isMatchedLine = (line, matchedLines = []) => {
const number = lineNumber(line)
return number !== null && matchedLines.includes(number)
}
const windowKey = (window, index) => `${window.start_line || 0}-${window.end_line || 0}-${index}`
const windowTitle = (window, index) => {
const startLine = window.start_line || 0
const endLine = window.end_line || 0
if (startLine || endLine) return `窗口 ${index + 1}: 第 ${startLine}-${endLine} 行`
return `窗口 ${index + 1}`
}
</script>
<style scoped lang="less">
.kb-document-preview {
border: 1px solid var(--gray-150);
border-radius: 8px;
background: var(--gray-0);
overflow: hidden;
.document-summary {
display: flex;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
background: var(--gray-25);
border-bottom: 1px solid var(--gray-100);
font-size: 12px;
color: var(--gray-700);
.summary-main {
font-weight: 600;
}
.summary-meta {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--gray-600);
}
}
.window-list {
display: flex;
flex-direction: column;
}
.window-card {
border-bottom: 1px solid var(--gray-100);
&:last-child {
border-bottom: none;
}
}
.window-title {
display: flex;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
font-size: 12px;
color: var(--gray-700);
background: var(--gray-10);
.matched-count {
color: var(--main-700);
white-space: nowrap;
}
}
.content-preview {
margin: 0;
padding: 10px 12px;
max-height: 360px;
overflow: auto;
background: var(--gray-0);
color: var(--gray-700);
font-size: 12px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.content-line {
display: block;
min-height: 1.6em;
&.matched {
background: var(--main-50);
color: var(--main-800);
}
}
.empty-text {
padding: 14px;
text-align: center;
color: var(--gray-600);
font-size: 12px;
}
}
</style>