temp:临时备份,完成一半知识库管理模块

This commit is contained in:
PingChuan
2025-12-01 12:33:53 +08:00
parent 754ec2c7b5
commit 0c1b81cfb2
25 changed files with 3564 additions and 560 deletions
@@ -0,0 +1,187 @@
import { useState, useEffect } from 'react';
import { Form, Input, Button, Card, message, Spin } from 'antd';
import { SaveOutlined } from '@ant-design/icons';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
import { updateDatasetName } from '~/api/dify-dataset/api/datasetApi';
const { TextArea } = Input;
interface DatasetSettingsProps {
dataset: Dataset | null;
onDatasetUpdated: (dataset: Dataset) => void;
}
/**
* 知识库设置组件
* 用于修改知识库名称和描述
*/
export default function DatasetSettings({
dataset,
onDatasetUpdated,
}: DatasetSettingsProps) {
const [form] = Form.useForm();
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// 初始化表单数据
useEffect(() => {
if (dataset) {
form.setFieldsValue({
name: dataset.name,
description: dataset.description || '',
});
setHasChanges(false);
}
}, [dataset, form]);
/**
* 处理表单值变化
*/
const handleValuesChange = () => {
const values = form.getFieldsValue();
const changed =
values.name !== dataset?.name ||
values.description !== (dataset?.description || '');
setHasChanges(changed);
};
/**
* 保存设置
*/
const handleSave = async () => {
if (!dataset) {
message.error('知识库不存在');
return;
}
try {
const values = await form.validateFields();
setSaving(true);
// 目前只支持修改名称
const updatedDataset = await updateDatasetName(dataset.id, values.name);
message.success('保存成功');
onDatasetUpdated(updatedDataset);
setHasChanges(false);
} catch (err: any) {
console.error('保存设置失败:', err);
message.error(err.message || '保存失败');
} finally {
setSaving(false);
}
};
/**
* 重置表单
*/
const handleReset = () => {
if (dataset) {
form.setFieldsValue({
name: dataset.name,
description: dataset.description || '',
});
setHasChanges(false);
}
};
if (!dataset) {
return (
<div className="settings-loading">
<Spin size="large" />
</div>
);
}
return (
<div className="dataset-settings-page">
{/* 页面标题 */}
<div className="page-header">
<h1></h1>
<p className="page-description">
</p>
</div>
{/* 设置表单 */}
<Card className="settings-card">
<Form
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
>
<Form.Item
name="name"
label="知识库名称"
rules={[
{ required: true, message: '请输入知识库名称' },
{ max: 100, message: '名称不能超过100个字符' },
]}
>
<Input placeholder="请输入知识库名称" maxLength={100} />
</Form.Item>
<Form.Item
name="description"
label="知识库描述"
extra="描述知识库的用途和内容(仅展示,暂不支持修改)"
>
<TextArea
placeholder="请输入知识库描述"
rows={4}
maxLength={500}
showCount
disabled
/>
</Form.Item>
{/* 只读信息 */}
<div className="readonly-info">
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
</span>
</div>
{/* <div className="info-item">
<span className="info-label">Embedding 模型:</span>
<span className="info-value">
{dataset.embedding_model || '默认模型'}
</span>
</div> */}
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">{dataset.document_count}</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">{dataset.word_count?.toLocaleString()}</span>
</div>
<div className="info-item">
<span className="info-label"></span>
<span className="info-value">
{new Date(dataset.created_at * 1000).toLocaleString('zh-CN')}
</span>
</div>
</div>
{/* 操作按钮 */}
<div className="form-actions">
<Button onClick={handleReset} disabled={!hasChanges}>
</Button>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={handleSave}
loading={saving}
disabled={!hasChanges}
>
</Button>
</div>
</Form>
</Card>
</div>
);
}
@@ -0,0 +1,367 @@
import { useState, useEffect } from 'react';
import {
Input,
Button,
InputNumber,
Checkbox,
Select,
Card,
Empty,
Spin,
message,
Divider,
Tooltip,
} from 'antd';
import {
QuestionCircleOutlined,
ReloadOutlined,
EyeOutlined,
} from '@ant-design/icons';
import type { Document } from '~/api/dify-dataset/type/documentTypes';
import type { Segment } from '~/api/dify-dataset/type';
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
import { updateDocumentWithSettings } from '~/api/dify-dataset/api/documentApi';
interface DocumentDetailProps {
datasetId: string;
document: Document | null;
}
/**
* 分段设置配置
* 注意:Dify API 支持的参数有限
* - separator: ✅ 支持
* - maxTokens: ✅ 支持
* - removeExtraSpaces: ✅ 支持
* - removeUrlsEmails: ✅ 支持
* - useQASegment: ⚠️ 需要 doc_form: "qa_model"
*/
interface SegmentationSettings {
separator: string;
maxTokens: number;
removeExtraSpaces: boolean;
removeUrlsEmails: boolean;
useQASegment: boolean;
qaLanguage: string;
}
/**
* 默认分段设置
*/
const DEFAULT_SETTINGS: SegmentationSettings = {
separator: '\\n\\n',
maxTokens: 500,
removeExtraSpaces: true,
removeUrlsEmails: false,
useQASegment: false,
qaLanguage: 'Chinese',
};
/**
* 文档详情组件
* 显示文档的分段设置,支持修改并重新处理
*/
export default function DocumentDetail({
datasetId,
document,
}: DocumentDetailProps) {
// 分段设置状态
const [settings, setSettings] = useState<SegmentationSettings>(DEFAULT_SETTINGS);
// 预览状态
const [previewSegments, setPreviewSegments] = useState<Segment[]>([]);
const [previewLoading, setPreviewLoading] = useState(false);
const [showPreview, setShowPreview] = useState(false);
// 保存状态
const [saving, setSaving] = useState(false);
// 当文档变化时重置设置
useEffect(() => {
if (document) {
// 可以从文档中读取已有的设置,这里使用默认值
setSettings(DEFAULT_SETTINGS);
setPreviewSegments([]);
setShowPreview(false);
}
}, [document?.id]);
/**
* 更新设置
*/
const updateSettings = (key: keyof SegmentationSettings, value: any) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
/**
* 重置设置
*/
const handleReset = () => {
setSettings(DEFAULT_SETTINGS);
setPreviewSegments([]);
setShowPreview(false);
};
/**
* 预览分段
*/
const handlePreview = async () => {
if (!document) return;
setPreviewLoading(true);
setShowPreview(true);
try {
// 获取当前文档的分段作为预览
const response = await fetchSegments(datasetId, document.id, 1, 50);
setPreviewSegments(response.data || []);
if (response.data?.length === 0) {
message.info('该文档暂无分段数据');
}
} catch (err: any) {
console.error('预览分段失败:', err);
message.error(err.message || '预览失败');
} finally {
setPreviewLoading(false);
}
};
/**
* 保存并处理
*/
const handleSaveAndProcess = async () => {
if (!document) return;
setSaving(true);
try {
await updateDocumentWithSettings(datasetId, document.id, {
indexing_technique: 'high_quality',
process_rule: {
mode: 'custom',
rules: {
pre_processing_rules: [
{ id: 'remove_extra_spaces', enabled: settings.removeExtraSpaces },
{ id: 'remove_urls_emails', enabled: settings.removeUrlsEmails },
],
segmentation: {
separator: settings.separator.replace(/\\n/g, '\n'),
max_tokens: settings.maxTokens,
},
},
},
});
message.success('设置已保存,文档正在重新处理...');
} catch (err: any) {
console.error('保存设置失败:', err);
message.error(err.message || '保存失败');
} finally {
setSaving(false);
}
};
if (!document) {
return (
<div className="document-detail-empty">
<Empty description="请选择一个文档" />
</div>
);
}
return (
<div className="document-detail-page">
<div className="document-detail-content">
{/* 左侧设置区域 */}
<div className="settings-panel">
{/* 分段设置 */}
<div className="settings-section">
<h3 className="section-title"></h3>
{/* 分块模式 */}
<div className="setting-item mode-selector">
<div className="mode-option active">
<div className="mode-icon">
<i className="ri-text-spacing"></i>
</div>
<div className="mode-info">
<span className="mode-name"></span>
<span className="mode-desc"></span>
</div>
</div>
</div>
{/* 分段标识符 */}
<div className="setting-item">
<label className="setting-label">
<Tooltip title="系统会在遇到指定分隔符时自动分段,默认值为 \n\n(按段落分段)">
<QuestionCircleOutlined className="help-icon" />
</Tooltip>
</label>
<Input
value={settings.separator}
onChange={(e) => updateSettings('separator', e.target.value)}
placeholder="\n\n"
className="setting-input"
/>
</div>
{/* 分段最大长度 */}
<div className="setting-item">
<label className="setting-label">
<Tooltip title="指定每个分段允许的最大字符数,超过此限制系统会强制分段">
<QuestionCircleOutlined className="help-icon" />
</Tooltip>
</label>
<div className="setting-input-with-suffix">
<InputNumber
value={settings.maxTokens}
onChange={(value) => updateSettings('maxTokens', value || 500)}
min={100}
max={4000}
className="setting-input-number"
/>
<span className="input-suffix">characters</span>
</div>
</div>
</div>
<Divider />
{/* 文本预处理规则 */}
<div className="settings-section">
<h3 className="section-title"></h3>
<div className="checkbox-group">
<Checkbox
checked={settings.removeExtraSpaces}
onChange={(e) => updateSettings('removeExtraSpaces', e.target.checked)}
>
</Checkbox>
<Checkbox
checked={settings.removeUrlsEmails}
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
>
URL
</Checkbox>
</div>
</div>
<Divider />
{/* Q&A 分段 */}
<div className="settings-section">
<div className="qa-segment-row">
<Checkbox
checked={settings.useQASegment}
onChange={(e) => updateSettings('useQASegment', e.target.checked)}
>
使 Q&A
</Checkbox>
<Select
value={settings.qaLanguage}
onChange={(value) => updateSettings('qaLanguage', value)}
disabled={!settings.useQASegment}
style={{ width: 120 }}
options={[
{ value: 'Chinese', label: 'Chinese' },
{ value: 'English', label: 'English' },
{ value: 'Japanese', label: 'Japanese' },
{ value: 'Korean', label: 'Korean' },
]}
/>
</div>
</div>
{/* 操作按钮 */}
<div className="settings-actions">
<Button
icon={<EyeOutlined />}
onClick={handlePreview}
loading={previewLoading}
>
</Button>
<Button
icon={<ReloadOutlined />}
onClick={handleReset}
>
</Button>
</div>
<Divider />
{/* 保存并处理按钮 */}
<div className="save-actions">
<Button
type="primary"
onClick={handleSaveAndProcess}
loading={saving}
block
>
</Button>
<Button block style={{ marginTop: 8 }}>
</Button>
</div>
</div>
{/* 右侧预览区域 */}
<div className="preview-panel">
<Card
title={
<div className="preview-header">
<span></span>
<Select
value={document.name}
style={{ width: 200 }}
disabled
options={[{ value: document.name, label: document.name }]}
/>
<span className="segment-count">
{showPreview ? `${previewSegments.length} 段块` : '0 段块'}
</span>
</div>
}
className="preview-card"
>
{previewLoading ? (
<div className="preview-loading">
<Spin size="large" />
<div className="loading-text">...</div>
</div>
) : !showPreview ? (
<div className="preview-empty">
<div className="empty-icon">
<EyeOutlined />
</div>
<p>"预览块"</p>
</div>
) : previewSegments.length === 0 ? (
<Empty description="暂无分段数据" />
) : (
<div className="preview-segments">
{previewSegments.map((segment, index) => (
<div key={segment.id} className="segment-item">
<div className="segment-header">
<span className="segment-index">#{index + 1}</span>
<span className="segment-chars">
{segment.word_count}
</span>
</div>
<div className="segment-content">
{segment.content}
</div>
</div>
))}
</div>
)}
</Card>
</div>
</div>
</div>
);
}
@@ -27,8 +27,8 @@ import {
PauseCircleOutlined,
} from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { Document, IndexingStatus } from '~/api/dify-dataset';
import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset';
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset/api/documentApi';
import '../../styles/components/dify-dataset-manager/index.css';
interface DocumentListProps {
@@ -43,6 +43,7 @@ interface DocumentListProps {
onDocumentDeleted: (documentId: string) => void;
onDocumentStatusChanged: (documentId: string, enabled: boolean) => void;
onRefresh: () => void;
onViewDocument?: (document: Document) => void;
}
/**
@@ -50,7 +51,6 @@ interface DocumentListProps {
*/
export default function DocumentList({
datasetId,
datasetName,
documents,
loading,
total,
@@ -60,6 +60,7 @@ export default function DocumentList({
onDocumentDeleted,
onDocumentStatusChanged,
onRefresh,
onViewDocument,
}: DocumentListProps) {
const [searchValue, setSearchValue] = useState('');
const [uploading, setUploading] = useState(false);
@@ -238,15 +239,12 @@ export default function DocumentList({
width: 120,
render: (_, record) => (
<Space size="small">
<Tooltip title="查看详情">
<Tooltip title="查看分段">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={() => {
// TODO: 查看文档详情/分段
message.info('功能开发中');
}}
onClick={() => onViewDocument?.(record)}
/>
</Tooltip>
<Popconfirm
@@ -273,11 +271,16 @@ export default function DocumentList({
];
return (
<div className="dataset-content">
{/* 头部区域 */}
<div className="dataset-header">
<h1>{datasetName || '知识库文档'}</h1>
<div className="dataset-header-actions">
<div className="document-list-page">
{/* 页面头部 */}
<div className="page-header">
<div className="header-left">
<h1></h1>
{/* <p className="page-description">
知识库的所有文件都在这里显示,整个知识库都可以被接到 Dify 引用或通过 Chat 插件进行索引。
</p> */}
</div>
<div className="header-actions">
<Tooltip title="刷新">
<Button
icon={<ReloadOutlined />}
@@ -297,36 +300,34 @@ export default function DocumentList({
loading={uploading}
disabled={!datasetId}
>
</Button>
</Upload>
</div>
</div>
{/* 工具栏 */}
<div className="document-list-toolbar">
{/* 搜索栏 */}
<div className="document-search-bar">
<Input
className="document-list-search"
placeholder="搜索文档..."
prefix={<SearchOutlined />}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
allowClear
style={{ width: 280 }}
/>
</div>
{/* 文档表格 - 固定表头和分页 */}
<div className="document-table-container">
{/* 文档表格 */}
<div className="document-table-wrapper">
{loading && documents.length === 0 ? (
<div className="dataset-loading">
<div className="loading-state">
<Spin size="large" />
<span className="text-gray-500">...</span>
<div className="loading-text">...</div>
</div>
) : filteredDocuments.length === 0 ? (
<div className="dataset-empty">
<Empty
description={searchValue ? '未找到匹配的文档' : '暂无文档'}
>
<div className="empty-state">
<Empty description={searchValue ? '未找到匹配的文档' : '暂无文档'}>
{!searchValue && (
<Upload
beforeUpload={handleUpload}
@@ -354,7 +355,7 @@ export default function DocumentList({
)}
</div>
{/* 固定底部分页器 */}
{/* 底部分页器 */}
{filteredDocuments.length > 0 && (
<div className="document-pagination">
<span className="pagination-total"> {total} </span>
+105 -18
View File
@@ -1,13 +1,19 @@
import { useEffect, useState } from 'react';
import { message, Spin } from 'antd';
import DatasetLayout, { type MenuTab } from './layout';
import DocumentList from './document-list';
import type { Dataset, Document } from '~/api/dify-dataset';
import { fetchDatasets, fetchDocuments } from '~/api/dify-dataset';
import DocumentDetail from './document-detail';
import RetrieveTest from './retrieve-test';
import DatasetSettings from './dataset-settings';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
import type { Document } from '~/api/dify-dataset/type/documentTypes';
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
import '../../styles/components/dify-dataset-manager/index.css';
/**
* 知识库管理主组件
* 简化版 - 假设只有一个知识库,直接显示文档列表
* 带左侧菜单栏的完整布局
*/
export default function DatasetManager() {
// 知识库状态
@@ -25,6 +31,12 @@ export default function DatasetManager() {
const [inited, setInited] = useState(false);
const [error, setError] = useState<string | null>(null);
// 菜单状态
const [activeTab, setActiveTab] = useState<MenuTab>('documents');
// 选中的文档(用于查看文档详情)
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
/**
* 加载知识库(获取第一个知识库)
*/
@@ -123,6 +135,39 @@ export default function DatasetManager() {
}
};
/**
* 查看文档详情(分段管理)
*/
const handleViewDocument = (doc: Document) => {
console.log('[DatasetManager] 查看文档详情:', doc);
setSelectedDocument(doc);
};
/**
* 返回文档列表
*/
const handleBackToDocuments = () => {
setSelectedDocument(null);
};
/**
* 处理菜单切换
*/
const handleTabChange = (tab: MenuTab) => {
setActiveTab(tab);
// 切换菜单时清除选中的文档
if (tab !== 'documents') {
setSelectedDocument(null);
}
};
/**
* 处理知识库更新
*/
const handleDatasetUpdated = (updatedDataset: Dataset) => {
setDataset(updatedDataset);
};
// 初始化
useEffect(() => {
loadDataset();
@@ -132,11 +177,9 @@ export default function DatasetManager() {
if (!inited || loadingDataset) {
return (
<div className="dataset-manager-wrapper">
<div className="dataset-manager-card">
<div className="dataset-loading-state">
<Spin size="large" />
<span className="loading-text">...</span>
</div>
<div className="dataset-loading-state">
<Spin size="large" />
<span className="loading-text">...</span>
</div>
</div>
);
@@ -146,20 +189,32 @@ export default function DatasetManager() {
if (error) {
return (
<div className="dataset-manager-wrapper">
<div className="dataset-manager-card">
<div className="dataset-error-state">
<i className="ri-error-warning-line error-icon"></i>
<h3></h3>
<p>{error}</p>
</div>
<div className="dataset-error-state">
<i className="ri-error-warning-line error-icon"></i>
<h3></h3>
<p>{error}</p>
</div>
</div>
);
}
return (
<div className="dataset-manager-wrapper">
<div className="dataset-manager-card">
/**
* 渲染右侧内容区
*/
const renderContent = () => {
// 文档菜单
if (activeTab === 'documents') {
// 如果选中了文档,显示文档详情
if (selectedDocument) {
return (
<DocumentDetail
datasetId={dataset?.id || ''}
document={selectedDocument}
/>
);
}
// 否则显示文档列表
return (
<DocumentList
datasetId={dataset?.id || ''}
datasetName={dataset?.name || ''}
@@ -172,8 +227,40 @@ export default function DatasetManager() {
onDocumentDeleted={handleDocumentDeleted}
onDocumentStatusChanged={handleDocumentStatusChanged}
onRefresh={handleRefresh}
onViewDocument={handleViewDocument}
/>
</div>
);
}
// 召回测试菜单
if (activeTab === 'retrieve') {
return <RetrieveTest datasetId={dataset?.id || ''} />;
}
// 设置菜单
if (activeTab === 'settings') {
return (
<DatasetSettings
dataset={dataset}
onDatasetUpdated={handleDatasetUpdated}
/>
);
}
return null;
};
return (
<div className="dataset-manager-wrapper">
<DatasetLayout
dataset={dataset}
activeTab={activeTab}
onTabChange={handleTabChange}
showBackButton={activeTab === 'documents' && !!selectedDocument}
onBack={handleBackToDocuments}
>
{renderContent()}
</DatasetLayout>
</div>
);
}
@@ -0,0 +1,109 @@
import { ReactNode } from 'react';
import { Button, Tooltip } from 'antd';
import {
FileTextOutlined,
SearchOutlined,
SettingOutlined,
ArrowLeftOutlined,
DatabaseOutlined,
} from '@ant-design/icons';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
/**
* 菜单项类型
*/
export type MenuTab = 'documents' | 'retrieve' | 'settings';
interface DatasetLayoutProps {
/** 知识库信息 */
dataset: Dataset | null;
/** 当前激活的菜单 */
activeTab: MenuTab;
/** 菜单切换回调 */
onTabChange: (tab: MenuTab) => void;
/** 是否显示返回按钮(在文档详情页时显示) */
showBackButton?: boolean;
/** 返回按钮点击回调 */
onBack?: () => void;
/** 子组件 */
children: ReactNode;
}
/**
* 知识库布局组件
* 包含左侧菜单栏和右侧内容区
*/
export default function DatasetLayout({
dataset,
activeTab,
onTabChange,
showBackButton = false,
onBack,
children,
}: DatasetLayoutProps) {
const menuItems: { key: MenuTab; icon: ReactNode; label: string }[] = [
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' },
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
];
return (
<div className="dataset-layout">
{/* 左侧侧边栏 */}
<aside className="dataset-sidebar">
{/* 返回按钮 */}
{showBackButton && onBack && (
<div className="sidebar-back">
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
className="back-btn"
>
</Button>
</div>
)}
{/* 知识库信息 */}
<div className="sidebar-header">
<div className="dataset-icon">
<DatabaseOutlined />
</div>
<div className="dataset-info">
<Tooltip title={dataset?.name} placement="right">
<h2 className="dataset-name">{dataset?.name || '知识库'}</h2>
</Tooltip>
<span className="dataset-type"></span>
</div>
</div>
{/* 统计信息 */}
<div className="sidebar-stats">
<span className="stat-item">
{dataset?.document_count || 0}
</span>
</div>
{/* 导航菜单 */}
<nav className="sidebar-menu">
{menuItems.map((item) => (
<button
key={item.key}
className={`menu-item ${activeTab === item.key ? 'active' : ''}`}
onClick={() => onTabChange(item.key)}
>
<span className="menu-icon">{item.icon}</span>
<span className="menu-label">{item.label}</span>
</button>
))}
</nav>
</aside>
{/* 右侧内容区 */}
<main className="dataset-main">
{children}
</main>
</div>
);
}
@@ -0,0 +1,202 @@
import { useState } from 'react';
import {
Input,
Button,
Card,
Select,
Slider,
Table,
Tag,
Empty,
Spin,
message,
} from 'antd';
import { FileSearchOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import type { RetrieveRecord } from '~/api/dify-dataset/type';
import { retrieveDataset } from '~/api/dify-dataset/api/segmentApi';
interface RetrieveTestProps {
datasetId: string;
}
/**
* 召回测试组件
* 用于测试知识库的检索效果
*/
export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
const [searchQuery, setSearchQuery] = useState('');
const [retrieveResults, setRetrieveResults] = useState<RetrieveRecord[]>([]);
const [retrieving, setRetrieving] = useState(false);
const [searchMethod, setSearchMethod] = useState<string>('hybrid_search');
const [topK, setTopK] = useState<number>(5);
/**
* 执行检索
*/
const handleRetrieve = async () => {
if (!searchQuery.trim()) {
message.warning('请输入检索关键词');
return;
}
if (!datasetId) {
message.warning('知识库ID不存在');
return;
}
setRetrieving(true);
try {
const response = await retrieveDataset(datasetId, searchQuery, {
search_method: searchMethod as any,
top_k: topK,
});
setRetrieveResults(response.records || []);
if (response.records?.length === 0) {
message.info('未找到匹配的结果');
}
} catch (err: any) {
console.error('检索失败:', err);
message.error(err.message || '检索失败');
} finally {
setRetrieving(false);
}
};
// 检索结果列定义
const columns: ColumnsType<RetrieveRecord> = [
{
title: '相关度',
dataIndex: 'score',
key: 'score',
width: 100,
render: (score: number) => (
<Tag color={score > 0.8 ? 'green' : score > 0.5 ? 'orange' : 'default'}>
{(score * 100).toFixed(1)}%
</Tag>
),
},
{
title: '内容',
key: 'content',
render: (_, record) => (
<div className="retrieve-result-content">
<div className="content-text">
{record.segment.content.length > 300
? record.segment.content.substring(0, 300) + '...'
: record.segment.content}
</div>
{record.segment.answer && (
<div className="answer-text">
<strong></strong>
{record.segment.answer.length > 150
? record.segment.answer.substring(0, 150) + '...'
: record.segment.answer}
</div>
)}
</div>
),
},
{
title: '字数',
key: 'word_count',
width: 80,
render: (_, record) => record.segment.word_count,
},
{
title: '命中次数',
key: 'hit_count',
width: 100,
render: (_, record) => record.segment.hit_count,
},
];
return (
<div className="retrieve-test-page">
{/* 页面标题 */}
<div className="page-header">
<h1></h1>
<p className="page-description">
</p>
</div>
{/* 检索设置 */}
<Card className="retrieve-settings" size="small">
<div className="search-row">
<Input
placeholder="输入检索关键词..."
prefix={<FileSearchOutlined />}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onPressEnter={handleRetrieve}
className="search-input"
/>
<Button
type="primary"
onClick={handleRetrieve}
loading={retrieving}
>
</Button>
</div>
<div className="options-row">
<div className="option-item">
<span className="option-label"></span>
<Select
value={searchMethod}
onChange={setSearchMethod}
style={{ width: 140 }}
options={[
{ value: 'keyword_search', label: '关键词搜索' },
{ value: 'semantic_search', label: '语义搜索' },
{ value: 'full_text_search', label: '全文搜索' },
{ value: 'hybrid_search', label: '混合搜索' },
]}
/>
</div>
<div className="option-item">
<span className="option-label"> (Top K)</span>
<Slider
value={topK}
onChange={setTopK}
min={1}
max={20}
style={{ width: 120 }}
/>
<span className="option-value">{topK}</span>
</div>
</div>
</Card>
{/* 检索结果 */}
<div className="retrieve-results">
{retrieving ? (
<div className="loading-state">
<Spin size="large" />
<div className="loading-text">...</div>
</div>
) : retrieveResults.length === 0 ? (
<Empty
description="请输入关键词进行检索"
className="empty-state"
/>
) : (
<>
<div className="results-header">
<span> {retrieveResults.length} </span>
</div>
<Table
columns={columns}
dataSource={retrieveResults}
rowKey={(record) => record.segment.id}
pagination={false}
size="small"
/>
</>
)}
</div>
</div>
);
}