Files
leaudit-platform-frontend/app/components/dify-dataset-manager/document-detail.tsx
T

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>
);
}