feat: 知识库朴素语义搜索(输入→检索→结果列表)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag } from 'antd'
|
||||
import { DeleteOutlined, ReloadOutlined, UploadOutlined } from '@ant-design/icons'
|
||||
import { Card, Upload, Table, Button, Space, Select, Popconfirm, message, Tag, Input } from 'antd'
|
||||
import { DeleteOutlined, ReloadOutlined, UploadOutlined, SearchOutlined, ClearOutlined } from '@ant-design/icons'
|
||||
import useAuthStore from '../../stores/authStore'
|
||||
import knowledgeService from '../../services/knowledgeService'
|
||||
import KnowledgeTree from '../../components/KnowledgeTree/KnowledgeTree'
|
||||
@@ -34,6 +34,11 @@ export default function KnowledgeBase() {
|
||||
const [sourceDetailFilter, setSourceDetailFilter] = useState('')
|
||||
const [selectedTreeNode, setSelectedTreeNode] = useState(null) // { type, detail }
|
||||
|
||||
// 搜索状态
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [searchResults, setSearchResults] = useState([])
|
||||
const [searchLoading, setSearchLoading] = useState(false)
|
||||
|
||||
const fetchItems = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -109,6 +114,30 @@ export default function KnowledgeBase() {
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
setSearchResults([])
|
||||
return
|
||||
}
|
||||
setSearchLoading(true)
|
||||
try {
|
||||
const data = await knowledgeService.searchItems(searchQuery.trim())
|
||||
setSearchResults(data.results || [])
|
||||
} catch {
|
||||
message.error('搜索失败')
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setSearchLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索 → 恢复树形浏览(不丢树状态)
|
||||
const handleClearSearch = () => {
|
||||
setSearchQuery('')
|
||||
setSearchResults([])
|
||||
}
|
||||
|
||||
// 树节点选中 → 联动过滤
|
||||
const handleNodeSelect = (node) => {
|
||||
setSelectedTreeNode(node)
|
||||
@@ -229,6 +258,107 @@ export default function KnowledgeBase() {
|
||||
<span style={{ fontSize: 12, color: '#aaa' }}>支持批量上传,系统自动解析 yaml frontmatter 并生成语义向量</span>
|
||||
</div>
|
||||
|
||||
{/* 搜索栏(搜索模式/浏览模式互斥,清空恢复树形浏览不丢状态) */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<Input
|
||||
placeholder="输入一段文字,语义搜索知识库…"
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
style={{ width: 340, borderRadius: 10 }}
|
||||
prefix={<SearchOutlined style={{ color: '#aaa' }} />}
|
||||
suffix={
|
||||
searchQuery ? (
|
||||
<ClearOutlined
|
||||
style={{ color: '#aaa', cursor: 'pointer' }}
|
||||
onClick={handleClearSearch}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
icon={<SearchOutlined />}
|
||||
onClick={handleSearch}
|
||||
loading={searchLoading}
|
||||
style={{ borderRadius: 10 }}
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
{searchQuery && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>
|
||||
{searchResults.length > 0
|
||||
? `找到 ${searchResults.length} 条相关结果`
|
||||
: '无相关结果'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 搜索结果列表(搜索模式显示,浏览模式隐藏) */}
|
||||
{searchQuery && searchResults.length > 0 && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<div style={{ fontSize: 13, color: '#666', marginBottom: 8, fontWeight: 500 }}>
|
||||
搜索结果
|
||||
</div>
|
||||
{searchResults.map(item => (
|
||||
<Card
|
||||
key={item.id}
|
||||
size="small"
|
||||
style={{ borderRadius: 10, marginBottom: 8 }}
|
||||
bodyStyle={{ padding: '12px 16px' }}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontWeight: 600, fontSize: 14, color: '#3b4a3b', marginBottom: 4 }}>
|
||||
{item.title}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}>
|
||||
<Tag color="default" style={{ borderRadius: 6, fontSize: 12 }}>
|
||||
{SOURCE_TYPE_LABEL[item.source_type] || item.source_type}
|
||||
</Tag>
|
||||
{item.author && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>作者:{item.author}</span>
|
||||
)}
|
||||
{item.source_detail && (
|
||||
<span style={{ fontSize: 12, color: '#888' }}>出处:{item.source_detail}</span>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#555',
|
||||
background: '#f5f5f5',
|
||||
borderRadius: 6,
|
||||
padding: '6px 10px',
|
||||
lineHeight: 1.6,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{item.snippet}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{
|
||||
marginLeft: 16,
|
||||
minWidth: 56,
|
||||
textAlign: 'center',
|
||||
background: item.similarity >= 0.7 ? '#e6f4ff' : '#f5f5f5',
|
||||
borderRadius: 8,
|
||||
padding: '4px 8px',
|
||||
}}>
|
||||
<div style={{ fontSize: 18, fontWeight: 700, color: item.similarity >= 0.7 ? '#1677ff' : '#666' }}>
|
||||
{Math.max(0, Math.round(item.similarity * 100))}%
|
||||
</div>
|
||||
<div style={{ fontSize: 11, color: '#888' }}>相关度</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 主体:左侧树 + 右侧列表(flex 左右并列,宽度稳定) */}
|
||||
<div style={{ display: 'flex', gap: 12, alignItems: 'flex-start' }}>
|
||||
{/* 左侧树 */}
|
||||
|
||||
@@ -52,6 +52,19 @@ const knowledgeService = {
|
||||
const resp = await http.get('/knowledge/grouped')
|
||||
return resp.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 语义检索:输入一段文字,返回最相关的知识库条目
|
||||
* @param {string} queryText - 查询文字
|
||||
* @param {number} topK - 返回条数,默认 5
|
||||
*/
|
||||
async searchItems(queryText, topK = 5) {
|
||||
const resp = await http.post('/knowledge/search', {
|
||||
query: queryText,
|
||||
top_k: topK,
|
||||
})
|
||||
return resp.data
|
||||
},
|
||||
}
|
||||
|
||||
export default knowledgeService
|
||||
Reference in New Issue
Block a user