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)

)}
) : (