248 lines
10 KiB
TypeScript
248 lines
10 KiB
TypeScript
import {
|
||
Input,
|
||
Button,
|
||
InputNumber,
|
||
Checkbox,
|
||
Select,
|
||
Card,
|
||
Empty,
|
||
Spin,
|
||
Divider,
|
||
Tooltip,
|
||
} 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';
|
||
|
||
/**
|
||
* 文档详情组件
|
||
* 显示文档的分段设置,支持修改并重新处理
|
||
*/
|
||
export default function DocumentDetail({
|
||
datasetId,
|
||
document,
|
||
}: DocumentDetailProps) {
|
||
const {
|
||
settings,
|
||
previewSegments,
|
||
previewLoading,
|
||
showPreview,
|
||
saving,
|
||
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(按段落分段)">
|
||
<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>
|
||
);
|
||
}
|