Files
YG-Datasets/easy-dataset-main/components/text-split/MarkdownViewDialog.js

434 lines
13 KiB
JavaScript

'use client';
import React, { useState, useEffect, useRef } from 'react';
import {
Box,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
CircularProgress,
Typography,
Divider,
Chip,
Switch,
FormControlLabel,
Alert,
DialogContentText
} from '@mui/material';
import DeleteIcon from '@mui/icons-material/Delete';
import SaveIcon from '@mui/icons-material/Save';
import ReactMarkdown from 'react-markdown';
import { useTranslation } from 'react-i18next';
import 'github-markdown-css/github-markdown-light.css';
export default function MarkdownViewDialog({ open, text, onClose, projectId, onSaveSuccess }) {
const { t } = useTranslation();
const [customSplitMode, setCustomSplitMode] = useState(false);
const [splitPoints, setSplitPoints] = useState([]);
const [selectedText, setSelectedText] = useState('');
const [savedMessage, setSavedMessage] = useState('');
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
const contentRef = useRef(null);
const [chunksPreview, setChunksPreview] = useState([]);
// 根据分块点计算每个块的字数
const calculateChunksPreview = points => {
if (!text || !text.content) return [];
const content = text.content;
const sortedPoints = [...points].sort((a, b) => a.position - b.position);
const chunks = [];
let startPos = 0;
// 计算每个分块
for (let i = 0; i < sortedPoints.length; i++) {
const endPos = sortedPoints[i].position;
const chunkContent = content.substring(startPos, endPos);
if (chunkContent.trim().length > 0) {
chunks.push({
index: i + 1,
length: chunkContent.length,
preview: chunkContent.substring(0, 20) + (chunkContent.length > 20 ? '...' : '')
});
}
startPos = endPos;
}
// 添加最后一个分块
const lastChunkContent = content.substring(startPos);
if (lastChunkContent.trim().length > 0) {
chunks.push({
index: chunks.length + 1,
length: lastChunkContent.length,
preview: lastChunkContent.substring(0, 20) + (lastChunkContent.length > 20 ? '...' : '')
});
}
return chunks;
};
// 重置组件状态
useEffect(() => {
if (!open) {
setSplitPoints([]);
setCustomSplitMode(false);
setSelectedText('');
setSavedMessage('');
}
}, [open]);
// 当分块点变化时更新预览
useEffect(() => {
if (splitPoints.length > 0 && text?.content) {
const preview = calculateChunksPreview(splitPoints);
setChunksPreview(preview);
} else {
setChunksPreview([]);
}
}, [splitPoints, text?.content]);
// 处理用户选择文本事件
const handleTextSelection = () => {
if (!customSplitMode) return;
const selection = window.getSelection();
if (!selection.toString().trim()) return;
// 获取选择的文本内容和位置
const selectedContent = selection.toString();
// 计算选择位置在文档中的偏移量
const range = selection.getRangeAt(0);
const preCaretRange = range.cloneRange();
preCaretRange.selectNodeContents(contentRef.current);
preCaretRange.setEnd(range.endContainer, range.endOffset);
const position = preCaretRange.toString().length;
// 添加到分割点列表
const newPoint = {
id: Date.now(),
position,
preview: selectedContent.substring(0, 40) + (selectedContent.length > 40 ? '...' : '')
};
setSplitPoints(prev => [...prev, newPoint].sort((a, b) => a.position - b.position));
setSelectedText('');
};
// 删除分割点
const handleDeletePoint = id => {
setSplitPoints(prev => prev.filter(point => point.id !== id));
};
// 弹出确认对话框
const handleConfirmSave = () => {
setConfirmDialogOpen(true);
};
// 取消保存
const handleCancelSave = () => {
setConfirmDialogOpen(false);
};
// 确认并执行保存
const handleSavePoints = async () => {
// 输出调试信息
console.log('保存分块点时的数据:', {
projectId,
text: text
? {
fileId: text.fileId,
fileName: text.fileName,
contentLength: text.content ? text.content.length : 0
}
: null,
splitPointsCount: splitPoints.length
});
if (!text) {
setError(t('textSplit.missingRequiredData') + ': text 为空');
return;
}
if (!text.fileId) {
setError(t('textSplit.missingRequiredData') + ': fileId 不存在');
return;
}
if (!text.fileName) {
setError(t('textSplit.missingRequiredData') + ': fileName 不存在');
return;
}
if (!text.content) {
setError(t('textSplit.missingRequiredData') + ': content 不存在');
return;
}
if (!projectId) {
setError(t('textSplit.missingRequiredData') + ': projectId 不存在');
return;
}
setConfirmDialogOpen(false);
setSaving(true);
setError('');
try {
// 准备要发送的数据
const customSplitData = {
fileId: text.fileId,
fileName: text.fileName,
content: text.content,
splitPoints: splitPoints.map(point => ({
position: point.position,
preview: point.preview
}))
};
// 发送请求到待创建的API接口
const response = await fetch(`/api/projects/${projectId}/custom-split`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(customSplitData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || t('textSplit.customSplitFailed'));
}
// 保存成功
setSavedMessage(t('textSplit.customSplitSuccess'));
// 短暂显示成功消息后关闭对话框并刷新列表
setTimeout(() => {
setSavedMessage('');
// 关闭对话框
onClose();
// 调用父组件的刷新方法(如果提供了)
if (typeof onSaveSuccess === 'function') {
onSaveSuccess();
}
}, 1500);
} catch (err) {
console.error('保存自定义分块出错:', err);
setError(err.message || t('textSplit.customSplitFailed'));
} finally {
setSaving(false);
}
};
return (
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
<DialogTitle sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography variant="h6">{text ? text.fileName : ''}</Typography>
<FormControlLabel
control={
<Switch checked={customSplitMode} onChange={e => setCustomSplitMode(e.target.checked)} color="primary" />
}
label={t('textSplit.customSplitMode')}
sx={{ ml: 2 }}
/>
</DialogTitle>
{customSplitMode && (
<Box sx={{ px: 3, py: 1, bgcolor: 'action.hover' }}>
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
{t('textSplit.customSplitInstructions')}
</Typography>
{/* 分割点列表 */}
{splitPoints.length > 0 && (
<Box sx={{ mt: 1, mb: 2 }}>
<Typography variant="body2" sx={{ mb: 1 }}>
{t('textSplit.splitPointsList')} ({splitPoints.length}):
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{splitPoints.map((point, index) => (
<Chip
key={point.id}
label={`${index + 1}. ${point.preview}`}
onDelete={() => handleDeletePoint(point.id)}
deleteIcon={<DeleteIcon />}
color="primary"
variant="outlined"
/>
))}
</Box>
{/* 文本块字数预览 */}
{chunksPreview.length > 0 && (
<Box
sx={{
mt: 2,
p: 1,
bgcolor: 'background.paper',
borderRadius: 1,
border: '1px dashed',
borderColor: 'divider'
}}
>
<Typography variant="subtitle2" gutterBottom>
{t('textSplit.chunksPreview')}
</Typography>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{chunksPreview.map(chunk => (
<Chip
key={chunk.index}
size="small"
label={`${t('textSplit.chunk')} ${chunk.index}: ${chunk.length}${t('textSplit.characters')}`}
color="info"
variant="outlined"
/>
))}
</Box>
</Box>
)}
</Box>
)}
{/* 保存按钮 */}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 1 }}>
<Button
variant="contained"
startIcon={<SaveIcon />}
disabled={splitPoints.length === 0 || saving}
onClick={handleConfirmSave}
size="small"
>
{saving ? t('common.saving') : t('textSplit.saveSplitPoints')}
</Button>
</Box>
{/* 提示消息 */}
{savedMessage && (
<Alert severity="success" sx={{ mt: 1 }}>
{savedMessage}
</Alert>
)}
{error && (
<Alert severity="error" sx={{ mt: 1 }}>
{error}
</Alert>
)}
</Box>
)}
<Divider />
<DialogContent dividers>
{text ? (
<Box
sx={{
maxHeight: '60vh',
overflow: 'auto',
cursor: customSplitMode ? 'text' : 'default',
position: 'relative',
'::selection': {
backgroundColor: customSplitMode ? 'primary.light' : 'inherit',
color: customSplitMode ? 'primary.contrastText' : 'inherit'
}
}}
onMouseUp={handleTextSelection}
ref={contentRef}
>
{/* 渲染带有分割点标记的内容 */}
{customSplitMode && splitPoints.length > 0 ? (
<Box>
<pre style={{ whiteSpace: 'pre-wrap', wordWrap: 'break-word', fontFamily: 'inherit' }}>
{text.content.split('').map((char, index) => {
const isSplitPoint = splitPoints.some(point => point.position === index);
const splitPointIndex = splitPoints.findIndex(point => point.position === index);
if (isSplitPoint) {
return (
<React.Fragment key={index}>
<span
style={{
display: 'inline-block',
width: '100%',
borderTop: '2px dashed #1976d2',
marginTop: '8px',
marginBottom: '8px',
position: 'relative'
}}
>
<span
style={{
position: 'absolute',
left: '0',
top: '-15px',
backgroundColor: '#1976d2',
color: 'white',
padding: '0 6px',
borderRadius: '4px',
fontSize: '12px'
}}
>
{splitPointIndex + 1}
</span>
</span>
{char}
</React.Fragment>
);
}
return char;
})}
</pre>
</Box>
) : (
<Box>
<div className="markdown-body">
<ReactMarkdown>{text.content}</ReactMarkdown>
</div>
</Box>
)}
</Box>
) : (
<Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}>
<CircularProgress />
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose}>{t('common.close')}</Button>
</DialogActions>
{/* 确认对话框 */}
<Dialog
open={confirmDialogOpen}
onClose={handleCancelSave}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{t('textSplit.confirmCustomSplitTitle')}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{t('textSplit.confirmCustomSplitMessage')}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleCancelSave}>{t('common.cancel')}</Button>
<Button onClick={handleSavePoints} color="primary" variant="contained" autoFocus>
{t('common.confirm')}
</Button>
</DialogActions>
</Dialog>
</Dialog>
);
}