import React, { useState, useEffect, useRef } from 'react';
import { useNavigate, Link, useLocation } from 'react-router-dom';
import toast from 'react-hot-toast';
import {
ArrowLeft, ArrowRight, FileText, Upload, Play, Download,
CheckCircle, AlertCircle, Loader2, Settings, Eye, Edit3,
Trash2, RefreshCw, FileUp, X, ChevronDown, ChevronUp,
Hash, Type, List, BookOpen, Quote, Table, Image, Code,
AlertTriangle, Info, Search, Filter
} from 'lucide-react';
import { wordFormatterAPI } from '../api';
// Paragraph type configuration with icons and colors
const PARAGRAPH_TYPES = {
title_cn: { label: '中文标题', icon: Type, color: 'bg-blue-100 text-blue-700 border-blue-300' },
title_en: { label: '英文标题', icon: Type, color: 'bg-blue-100 text-blue-700 border-blue-300' },
heading_1: { label: '一级标题', icon: Hash, color: 'bg-blue-100 text-blue-700 border-blue-300' },
heading_2: { label: '二级标题', icon: Hash, color: 'bg-cyan-100 text-cyan-700 border-cyan-300' },
heading_3: { label: '三级标题', icon: Hash, color: 'bg-teal-100 text-teal-700 border-teal-300' },
heading_4: { label: '四级标题', icon: Hash, color: 'bg-emerald-100 text-emerald-700 border-emerald-300' },
heading_5: { label: '五级标题', icon: Hash, color: 'bg-lime-100 text-lime-700 border-lime-300' },
heading_6: { label: '六级标题', icon: Hash, color: 'bg-green-100 text-green-700 border-green-300' },
abstract_cn: { label: '中文摘要', icon: BookOpen, color: 'bg-amber-100 text-amber-700 border-amber-300' },
abstract_en: { label: '英文摘要', icon: BookOpen, color: 'bg-amber-100 text-amber-700 border-amber-300' },
keywords_cn: { label: '中文关键词', icon: List, color: 'bg-orange-100 text-orange-700 border-orange-300' },
keywords_en: { label: '英文关键词', icon: List, color: 'bg-orange-100 text-orange-700 border-orange-300' },
body: { label: '正文', icon: FileText, color: 'bg-gray-100 text-gray-700 border-gray-300' },
reference: { label: '参考文献', icon: BookOpen, color: 'bg-teal-100 text-teal-700 border-teal-300' },
acknowledgement: { label: '致谢', icon: BookOpen, color: 'bg-pink-100 text-pink-700 border-pink-300' },
figure_caption: { label: '图题', icon: Image, color: 'bg-rose-100 text-rose-700 border-rose-300' },
table_caption: { label: '表题', icon: Table, color: 'bg-blue-100 text-blue-700 border-blue-300' },
list_item: { label: '列表项', icon: List, color: 'bg-green-100 text-green-700 border-green-300' },
toc: { label: '目录', icon: List, color: 'bg-slate-100 text-slate-700 border-slate-300' },
code_block: { label: '代码块', icon: Code, color: 'bg-zinc-100 text-zinc-700 border-zinc-300' },
blockquote: { label: '引用块', icon: Quote, color: 'bg-sky-100 text-sky-700 border-sky-300' },
};
// Issue severity icons and colors
const SEVERITY_CONFIG = {
error: { icon: AlertCircle, color: 'text-red-500 bg-red-50 border-red-200', label: '错误' },
warning: { icon: AlertTriangle, color: 'text-yellow-600 bg-yellow-50 border-yellow-200', label: '警告' },
info: { icon: Info, color: 'text-blue-500 bg-blue-50 border-blue-200', label: '提示' },
};
const FormatCheckerPage = () => {
const navigate = useNavigate();
const location = useLocation();
const fileInputRef = useRef(null);
const editingPanelRef = useRef(null);
// Input mode and content
const [inputMode, setInputMode] = useState('file'); // 'file' or 'text'
const [text, setText] = useState('');
const [file, setFile] = useState(null);
const [dragActive, setDragActive] = useState(false);
// Configuration
const [showConfig, setShowConfig] = useState(false);
const [checkMode, setCheckMode] = useState('loose'); // 'loose' or 'strict'
// Check state
const [isChecking, setIsChecking] = useState(false);
const [checkResult, setCheckResult] = useState(null);
// Result state
const [paragraphs, setParagraphs] = useState([]);
const [issues, setIssues] = useState([]);
const [markedText, setMarkedText] = useState('');
const [editingIndex, setEditingIndex] = useState(null);
// View mode
const [viewMode, setViewMode] = useState('list'); // 'list', 'issues', 'raw'
const [issueFilter, setIssueFilter] = useState('all'); // 'all', 'error', 'warning', 'info'
const [usage, setUsage] = useState(null);
// Check if coming from spec generator with a spec
const selectedSpec = location.state?.specJson || null;
const specName = location.state?.specName || null;
// Handle click outside to close editing panel
useEffect(() => {
const handleClickOutside = (event) => {
if (editingIndex !== null && editingPanelRef.current && !editingPanelRef.current.contains(event.target)) {
setEditingIndex(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [editingIndex]);
useEffect(() => {
loadUsage();
}, []);
const loadUsage = async () => {
try {
const response = await wordFormatterAPI.getUsage();
setUsage(response.data);
} catch (error) {
console.error('Load usage failed:', error);
}
};
// File handling
const handleFileChange = (e) => {
const selectedFile = e.target.files?.[0];
if (selectedFile) {
validateAndSetFile(selectedFile);
}
};
const validateAndSetFile = (selectedFile) => {
const allowedTypes = [
'text/plain',
'text/markdown',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
];
const allowedExtensions = ['.txt', '.md', '.docx'];
const ext = selectedFile.name.substring(selectedFile.name.lastIndexOf('.')).toLowerCase();
if (!allowedTypes.includes(selectedFile.type) && !allowedExtensions.includes(ext)) {
toast.error('仅支持 .txt, .md, .docx 文件');
return;
}
if (selectedFile.size > 10 * 1024 * 1024) {
toast.error('文件大小不能超过 10MB');
return;
}
setFile(selectedFile);
toast.success(`已选择文件: ${selectedFile.name}`);
};
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'dragenter' || e.type === 'dragover') {
setDragActive(true);
} else if (e.type === 'dragleave') {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files?.[0]) {
validateAndSetFile(e.dataTransfer.files[0]);
}
};
// Start format check
const handleStartCheck = async () => {
if (inputMode === 'file' && !file) {
toast.error('请选择文件');
return;
}
if (inputMode === 'text' && !text.trim()) {
toast.error('请输入文本内容');
return;
}
try {
setIsChecking(true);
setParagraphs([]);
setIssues([]);
setMarkedText('');
setCheckResult(null);
let response;
if (inputMode === 'file') {
response = await wordFormatterAPI.checkFileFormat(file, checkMode);
} else {
response = await wordFormatterAPI.checkTextFormat(text, checkMode);
}
const data = response.data;
setCheckResult(data);
if (data.success) {
setParagraphs(data.paragraphs || []);
setIssues(data.issues || []);
setMarkedText(data.marked_text || '');
if (data.is_valid) {
toast.success('格式检测通过!文章符合规范。');
} else {
const errorCount = data.issues.filter(i => i.severity === 'error').length;
const warningCount = data.issues.filter(i => i.severity === 'warning').length;
toast.success(
`格式检测完成:${errorCount} 个错误,${warningCount} 个警告`,
{ duration: 4000 }
);
}
} else {
toast.error(data.error || '格式检测失败');
}
} catch (error) {
console.error('Format check failed:', error);
toast.error(error.response?.data?.detail || '格式检测失败');
} finally {
setIsChecking(false);
}
};
// Edit paragraph type
const handleTypeChange = (index, newType) => {
const updated = [...paragraphs];
updated[index] = { ...updated[index], paragraph_type: newType, is_auto_detected: false };
setParagraphs(updated);
setEditingIndex(null);
// Regenerate marked text
regenerateMarkedText(updated);
};
const regenerateMarkedText = (updatedParagraphs) => {
const lines = updatedParagraphs.map((p) => {
return `\n${p.text}`;
});
setMarkedText(lines.join('\n\n'));
};
// Export marked text
const handleExportMarkdown = () => {
if (!markedText) {
toast.error('没有可导出的内容');
return;
}
const blob = new Blob([markedText], { type: 'text/markdown;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file?.name?.replace(/\.[^.]+$/, '_marked.md') || 'article_marked.md';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
toast.success('已导出 Markdown 文件');
};
// Navigate to format page
const handleGoToFormat = () => {
if (!markedText) {
toast.error('请先完成格式检测');
return;
}
navigate('/word-formatter', {
state: {
preprocessedText: markedText,
specJson: selectedSpec,
specName: specName,
},
});
};
// Reset form
const handleReset = () => {
setFile(null);
setText('');
setCheckResult(null);
setParagraphs([]);
setIssues([]);
setMarkedText('');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
// Filter issues
const filteredIssues = issues.filter(issue => {
if (issueFilter === 'all') return true;
return issue.severity === issueFilter;
});
// Scroll to paragraph
const scrollToParagraph = (paragraphIndex) => {
setViewMode('list');
setTimeout(() => {
const element = document.getElementById(`paragraph-${paragraphIndex}`);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
element.classList.add('ring-2', 'ring-blue-500');
setTimeout(() => {
element.classList.remove('ring-2', 'ring-blue-500');
}, 2000);
}
}, 100);
};
// Render paragraph type badge
const renderTypeBadge = (type, index) => {
const config = PARAGRAPH_TYPES[type] || PARAGRAPH_TYPES.body;
const IconComponent = config.icon;
const isEditing = editingIndex === index;
if (isEditing) {
return (
选择段落类型
{Object.entries(PARAGRAPH_TYPES).map(([key, cfg]) => {
const Icon = cfg.icon;
return (
);
})}
);
}
return (
);
};
// Render issue item
const renderIssue = (issue, idx) => {
const config = SEVERITY_CONFIG[issue.severity] || SEVERITY_CONFIG.info;
const Icon = config.icon;
return (
issue.paragraph_index >= 0 && scrollToParagraph(issue.paragraph_index)}
>
行 {issue.line}
{config.label}
{issue.message}
{issue.suggestion}
{issue.content_preview && (
"{issue.content_preview}"
)}
);
};
return (
{/* Header */}
{usage && (
使用量: {usage.usage_count}/{usage.usage_limit > 0 ? usage.usage_limit : '∞'}
)}
{selectedSpec && (
已选规范: {specName || '自定义'}
)}
{/* Workflow indicator */}
1. 生成规范
2. 格式检测
3. 生成 Word
{/* Left Panel - Input */}
{/* Input Mode Toggle */}
{inputMode === 'file' ? (
{file ? (
{file.name}
{(file.size / 1024).toFixed(1)} KB
) : (
<>
拖拽文件到这里,或点击选择
支持 .txt, .md, .docx (最大 10MB)
>
)}
) : (
{/* Configuration */}
{showConfig && (
{checkMode === 'loose'
? '宽松模式:只检测关键格式问题(如标题层级跳跃)'
: '严格模式:检测所有格式问题,包括警告和提示'}
)}
{/* Action Buttons */}
{/* Issues Summary */}
{checkResult && issues.length > 0 && (
检测问题
{/* Issue filter */}
{[
{ key: 'all', label: '全部', count: issues.length },
{ key: 'error', label: '错误', count: issues.filter(i => i.severity === 'error').length },
{ key: 'warning', label: '警告', count: issues.filter(i => i.severity === 'warning').length },
{ key: 'info', label: '提示', count: issues.filter(i => i.severity === 'info').length },
].map(({ key, label, count }) => (
))}
{/* Issues list */}
{filteredIssues.map((issue, idx) => renderIssue(issue, idx))}
)}
{/* Right Panel - Result */}
{/* Result Header */}
{checkResult && paragraphs.length > 0 && (
检测结果
{/* Validity Status */}
{checkResult.is_valid ? (
<>
格式检测通过!
文章结构符合规范要求
>
) : (
<>
发现 {issues.length} 个问题
建议修正后再继续
>
)}
{/* Statistics */}
{paragraphs.filter((p) => p.paragraph_type.startsWith('heading')).length}
标题
{paragraphs.filter((p) => p.paragraph_type === 'body').length}
正文
{/* Content View */}
{viewMode === 'list' ? (
{paragraphs.map((para, index) => (
{index + 1}
{renderTypeBadge(para.paragraph_type, index)}
{!para.is_auto_detected && (
已手动修改
)}
{para.text}
))}
) : (
{markedText}
)}
{/* Action Buttons */}
)}
{/* Empty state */}
{!checkResult && (
等待格式检测
上传文件或粘贴文本后,点击"开始检测"按钮
系统将自动识别段落类型并检测格式问题
无需 AI 处理,检测速度更快
)}
);
};
export default FormatCheckerPage;