292 lines
13 KiB
TypeScript
292 lines
13 KiB
TypeScript
import {
|
|
Input,
|
|
Button,
|
|
InputNumber,
|
|
Checkbox,
|
|
Card,
|
|
Empty,
|
|
Spin,
|
|
Divider,
|
|
Tooltip,
|
|
Progress,
|
|
} from 'antd';
|
|
import {
|
|
QuestionCircleOutlined,
|
|
ReloadOutlined,
|
|
EyeOutlined,
|
|
} from '@ant-design/icons';
|
|
import { useDocumentDetail } from '~/hooks/dify-dataset-manager/document-detail';
|
|
import type { DocumentDetailProps } from '~/types/dify-dataset-manager/document-detail';
|
|
import { INDEXING_STATUS_CONFIG } from '~/types/dify-dataset-manager/document-detail';
|
|
|
|
/**
|
|
* 文档详情组件
|
|
* 显示文档的分段设置,支持修改并重新处理
|
|
*/
|
|
export default function DocumentDetail({
|
|
datasetId,
|
|
document,
|
|
}: DocumentDetailProps) {
|
|
const {
|
|
settings,
|
|
previewSegments,
|
|
previewLoading,
|
|
showPreview,
|
|
saving,
|
|
isProcessing,
|
|
indexingStatus,
|
|
updateSettings,
|
|
handleReset,
|
|
handlePreview,
|
|
handleSaveAndProcess,
|
|
} = useDocumentDetail(datasetId, document);
|
|
|
|
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和 \n 是常用于分隔段落和行的分隔符。用逗号连接分隔符(\n\n,\n)当段落超过最大块长度时,会按行进行分割。你也可以使用自定义的特殊分隔符(例如 ***)">
|
|
<QuestionCircleOutlined className="help-icon" />
|
|
</Tooltip>
|
|
</label>
|
|
<Input
|
|
value={settings.separator}
|
|
onChange={(e) => updateSettings('separator', e.target.value)}
|
|
placeholder="\n\n"
|
|
disabled={isProcessing}
|
|
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}
|
|
disabled={isProcessing}
|
|
className="setting-input-number"
|
|
/>
|
|
<span className="input-suffix">characters</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 分段重叠长度 */}
|
|
<div className="setting-item">
|
|
<label className="setting-label">
|
|
分段重叠长度
|
|
<Tooltip title="设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果建议设置为最大分段长度的10%-25%">
|
|
<QuestionCircleOutlined className="help-icon" />
|
|
</Tooltip>
|
|
</label>
|
|
<div className="setting-input-with-suffix">
|
|
<InputNumber
|
|
value={settings.chunkOverlap}
|
|
onChange={(value) => updateSettings('chunkOverlap', value || 50)}
|
|
min={0}
|
|
max={500}
|
|
disabled={isProcessing}
|
|
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)}
|
|
disabled={isProcessing}
|
|
>
|
|
替换掉连续的空格、换行符和制表符
|
|
</Checkbox>
|
|
|
|
<Checkbox
|
|
checked={settings.removeUrlsEmails}
|
|
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
|
|
disabled={isProcessing}
|
|
>
|
|
删除所有 URL 和电子邮件地址
|
|
</Checkbox>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
{/* 索引方式 */}
|
|
<div className="settings-section">
|
|
<h3 className="section-title">索引方式</h3>
|
|
<div className="index-options">
|
|
<div
|
|
className={`index-option ${settings.indexingTechnique === 'high_quality' ? 'active' : ''} ${isProcessing ? 'disabled' : ''}`}
|
|
onClick={() => !isProcessing && updateSettings('indexingTechnique', 'high_quality')}
|
|
>
|
|
<span className="option-radio"></span>
|
|
<span className="option-label">高质量</span>
|
|
<span className="option-badge recommended">推荐</span>
|
|
</div>
|
|
<div
|
|
className={`index-option ${settings.indexingTechnique === 'economy' ? 'active' : ''} ${isProcessing ? 'disabled' : ''}`}
|
|
onClick={() => !isProcessing && updateSettings('indexingTechnique', 'economy')}
|
|
>
|
|
<span className="option-radio"></span>
|
|
<span className="option-label">经济</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div className="settings-actions">
|
|
<Button
|
|
icon={<EyeOutlined />}
|
|
onClick={handlePreview}
|
|
loading={previewLoading}
|
|
disabled={isProcessing}
|
|
>
|
|
预览块
|
|
</Button>
|
|
<Button
|
|
icon={<ReloadOutlined />}
|
|
onClick={handleReset}
|
|
disabled={isProcessing}
|
|
>
|
|
重置
|
|
</Button>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
{/* 保存并处理按钮 */}
|
|
<div className="save-actions">
|
|
<Button
|
|
type="primary"
|
|
onClick={handleSaveAndProcess}
|
|
loading={saving}
|
|
disabled={isProcessing}
|
|
block
|
|
>
|
|
{isProcessing ? '处理中...' : '保存并处理'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 右侧预览区域 */}
|
|
<div className="preview-panel">
|
|
<Card
|
|
title={
|
|
<div className="preview-header">
|
|
<span>预览</span>
|
|
<div>{document.name}</div>
|
|
<span className="segment-count">
|
|
{showPreview ? `${previewSegments.length} 段块` : '0 段块'}
|
|
</span>
|
|
</div>
|
|
}
|
|
className="preview-card"
|
|
>
|
|
{/* 处理进度显示 */}
|
|
{isProcessing && indexingStatus && (
|
|
<div className="processing-status">
|
|
<div className="processing-title">
|
|
<Spin size="small" />
|
|
<span>正在处理文档...</span>
|
|
</div>
|
|
<Progress
|
|
percent={INDEXING_STATUS_CONFIG[indexingStatus]?.percent || 0}
|
|
status="active"
|
|
strokeColor={{ '0%': '#00684a', '100%': '#52c41a' }}
|
|
/>
|
|
<div className="status-text">
|
|
{INDEXING_STATUS_CONFIG[indexingStatus]?.text || '处理中...'}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 预览内容 */}
|
|
{!isProcessing && (
|
|
<>
|
|
{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>
|
|
);
|
|
}
|