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

368 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}