import React, { useState, useEffect, useRef } from 'react'; import { MainUI } from './components/MainUI'; import { Sidebar } from './components/Sidebar'; import { SplitterModal } from './components/SplitterModal'; import { EditorModal } from './components/EditorModal'; import { NameAnalysisModal } from './components/NameAnalysisModal'; import { GuideModal } from './components/GuideModal'; import { LogModal } from './components/LogModal'; import { ConfirmationModal } from './components/ConfirmationModal'; import { aiService } from './services/aiService'; import { dbService } from './services/dbService'; import { epubService } from './services/epubService'; import { logger } from './services/logger'; import { parseFile, generateEpub } from './services/fileParsers'; import { formatBookStyle, optimizeDictionary, optimizeContext } from './services/textUtils'; import { AVAILABLE_GENRES, PROMPT_TEMPLATES } from './constants'; function App() { // --- UI State --- const abortControllerRef = useRef(null); const [isDarkMode, setIsDarkMode] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showLogs, setShowLogs] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(true); // --- Database & Story State --- const [stories, setStories] = useState([]); const [storyToDelete, setStoryToDelete] = useState(null); const [currentStoryId, setCurrentStoryId] = useState(null); const autoSaveTimerRef = useRef(null); // --- Data State (Current Story) --- const [files, setFiles] = useState([]); const [storyInfo, setStoryInfo] = useState({ title: '', author: '', summary: '', genres: [], languages: ['Tiếng Trung'], mcPersonality: [], worldSetting: [], sectFlow: [], contextNotes: '', image_prompt: '' }); const [coverPreviewUrl, setCoverPreviewUrl] = useState(null); const [isGeneratingCover, setIsGeneratingCover] = useState(false); const [promptTemplate, setPromptTemplate] = useState('DEFAULT'); // --- Processing State --- const [isProcessing, setIsProcessing] = useState(false); const [progressPercentage, setProgressPercentage] = useState(0); const [startTime, setStartTime] = useState(null); // --- Logic State for Tabs --- const [quickInput, setQuickInput] = useState(''); const [isAutoAnalyzing, setIsAutoAnalyzing] = useState(false); // Modals & Tools const [showSplitter, setShowSplitter] = useState(false); const [pendingSplitFile, setPendingSplitFile] = useState(null); const [showEditor, setShowEditor] = useState(false); const [editorFile, setEditorFile] = useState(null); const [dictTab, setDictTab] = useState('default'); const [additionalDictionary, setAdditionalDictionary] = useState(''); const [viewOriginalPrompt, setViewOriginalPrompt] = useState(false); const [selectedTemplateKey, setSelectedTemplateKey] = useState('default'); const [isOptimizingPrompt, setIsOptimizingPrompt] = useState(false); const [showPromptDesigner, setShowPromptDesigner] = useState(false); const [showGuideModal, setShowGuideModal] = useState(false); const [showNameAnalysisModal, setShowNameAnalysisModal] = useState(false); const [isAnalyzingNames, setIsAnalyzingNames] = useState(false); const [analysisResults, setAnalysisResults] = useState(null); // Workspace const [selectedFiles, setSelectedFiles] = useState(new Set()); const [rangeStart, setRangeStart] = useState(''); const [rangeEnd, setRangeEnd] = useState(''); const [currentPage, setCurrentPage] = useState(0); const [showFilterPanel, setShowFilterPanel] = useState(false); const [filterStatuses, setFilterStatuses] = useState(new Set()); const [showFindReplace, setShowFindReplace] = useState(false); const [findText, setFindText] = useState(''); const [replaceText, setReplaceText] = useState(''); const [rangeMode, setRangeMode] = useState(true); // true = Select, false = Deselect // --- Initialization --- useEffect(() => { logger.init(); logger.info("Application Started"); // Init DB const initApp = async () => { try { await dbService.init(); const loadedStories = dbService.getStories(); setStories(loadedStories); if (loadedStories.length > 0) { // Auto-load most recent story handleSelectStory(loadedStories[0].id); } } catch (e) { console.error("DB Init Failed", e); alert("Lỗi khởi tạo Database! Vui lòng refresh trang."); } }; initApp(); }, []); // theme effect useEffect(() => { if (isDarkMode) document.documentElement.classList.add('dark'); else document.documentElement.classList.remove('dark'); }, [isDarkMode]); // --- DB Operations & Auto Save --- // Trigger Save whenever core data changes (Debounced) useEffect(() => { if (!currentStoryId) return; // Clear existing timer if (autoSaveTimerRef.current) clearTimeout(autoSaveTimerRef.current); autoSaveTimerRef.current = setTimeout(async () => { // 1. Update Story Info await dbService.updateStory(currentStoryId, { title: storyInfo.title, author: storyInfo.author, summary: storyInfo.summary, genres: storyInfo.genres, contextNotes: storyInfo.contextNotes, image_prompt: storyInfo.image_prompt, additionalDictionary: additionalDictionary, customPrompt: promptTemplate, cover_image: typeof coverPreviewUrl === 'string' && coverPreviewUrl.startsWith('data:') ? 'base64...' : storyInfo.cover_image }); // 2. Save Files await dbService.saveFilesBatch(currentStoryId, files); console.log(`[AutoSave] Saved Story ${currentStoryId}`); }, 2000); // 2s debounce return () => clearTimeout(autoSaveTimerRef.current); }, [files, storyInfo, additionalDictionary, promptTemplate, currentStoryId]); const refreshStories = () => { setStories(dbService.getStories()); }; const handleCreateStory = async () => { console.log("[App] handleCreateStory Triggered"); // START FIX: Bypass blocking prompt const timestamp = new Date().toLocaleString('vi-VN'); const defaultTitle = `Truyện Mới (${timestamp})`; console.log("[App] Auto-creating story:", defaultTitle); try { const id = await dbService.createStory(defaultTitle); console.log("[App] Story Created ID:", id); refreshStories(); handleSelectStory(id); } catch (e) { console.error("[App] Create Story Failed:", e); alert("Tạo truyện thất bại: " + e.message); } }; const handleDeleteStory = (id) => { const story = stories.find(s => s.id === id); if (story) setStoryToDelete(story); }; const confirmDeleteStory = async () => { if (!storyToDelete) return; await dbService.deleteStory(storyToDelete.id); refreshStories(); if (currentStoryId === storyToDelete.id) { setFiles([]); setStoryInfo({ title: '', author: '', summary: '', genres: [], languages: ['Tiếng Trung'], mcPersonality: [], worldSetting: [], sectFlow: [], contextNotes: '', image_prompt: '' }); setCurrentStoryId(null); } setStoryToDelete(null); }; const handleUpdateStory = async (id, data) => { await dbService.updateStory(id, data); refreshStories(); if (id === currentStoryId) { setStoryInfo(prev => ({ ...prev, ...data })); } }; const handleSelectStory = async (id) => { // 1. Load Info const story = dbService.getStory(id); if (!story) return; // 2. Load Files const loadedFiles = dbService.getFiles(id); // 3. Update State setCurrentStoryId(id); setStoryInfo(prev => ({ ...prev, title: story.title || '', author: story.author || '', summary: story.summary || '', genres: story.genres || [], contextNotes: story.contextNotes || '', image_prompt: story.image_prompt || '', })); setAdditionalDictionary(story.additionalDictionary || ''); setPromptTemplate(story.customPrompt || PROMPT_TEMPLATES['DEFAULT']?.content || ''); setFiles(loadedFiles); // Update cover if (story.cover_image) setCoverPreviewUrl(story.cover_image); else setCoverPreviewUrl(null); // Refresh list (no need to re-sort or bump timestamp for view) // await dbService.updateStory(id, {}); }; // --- Handlers (Existing) --- const toggleTheme = () => setIsDarkMode(!isDarkMode); const handleFileUpload = async (e) => { if (!currentStoryId) { alert("Vui lòng Chọn hoặc Tạo truyện mới trước khi upload file!"); return; } const uploadedFiles = Array.from(e.target.files); logger.info(`User selected ${uploadedFiles.length} files`); if (uploadedFiles.length === 1 && uploadedFiles[0].size > 10000) { const parsed = await parseFile(uploadedFiles[0]); if (parsed.length > 0) { logger.info(`Large file detected (${uploadedFiles[0].size}B), triggering Splitter`); setPendingSplitFile(parsed[0]); setShowSplitter(true); } return; } const newFiles = []; for (const file of uploadedFiles) { const parsed = await parseFile(file); newFiles.push(...parsed); } setFiles(prev => [...prev, ...newFiles]); }; const handleSplitConfirm = (splitFiles) => { logger.info(`Split confirmed: ${splitFiles.length} new files created`); setFiles(prev => [...prev, ...splitFiles]); setShowSplitter(false); setPendingSplitFile(null); }; const handleQuickParse = () => { if (!quickInput) return; const parts = quickInput.split(/[,;]+/).map(s => s.trim()).filter(s => s); const newGenres = [...storyInfo.genres]; parts.forEach(p => { if (AVAILABLE_GENRES.includes(p) && !newGenres.includes(p)) newGenres.push(p); }); setStoryInfo(prev => ({ ...prev, genres: newGenres })); setQuickInput(''); }; // Helper const blobToBase64 = (blob) => new Promise((resolve, _) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.readAsDataURL(blob); }); const handleAutoAnalyze = async () => { if (files.length === 0) { alert("Bạn chưa tải file nào lên!"); return; } setIsAutoAnalyzing(true); try { const { info: analysis, cover } = await aiService.smartAnalyze(files); setStoryInfo(prev => ({ ...prev, title: analysis.title || prev.title, author: analysis.author || prev.author, summary: analysis.summary || prev.summary, genres: analysis.genres || prev.genres, contextNotes: analysis.context_notes || prev.contextNotes, image_prompt: analysis.image_prompt || prev.image_prompt, mcPersonality: analysis.main_characters ? [...prev.mcPersonality, ...analysis.main_characters.map(c => typeof c === 'object' ? c.name : c)] : prev.mcPersonality })); if (cover) { if (coverPreviewUrl && !coverPreviewUrl.startsWith('http')) URL.revokeObjectURL(coverPreviewUrl); const url = typeof cover === 'string' ? cover : URL.createObjectURL(cover); setCoverPreviewUrl(url); // Save to DB (Handle both String URL and Blob Base64) const saveValue = typeof cover === 'string' ? cover : await blobToBase64(cover); await dbService.updateStory(currentStoryId, { cover_image: saveValue }); } alert("Phân tích hoàn tất!"); } catch (e) { console.error(e); alert("Phân tích thất bại: " + e.message); } finally { setIsAutoAnalyzing(false); } }; const handleRegenerateCover = async () => { if (!storyInfo.image_prompt) { alert("Chưa có Prompt vẽ ảnh."); return; } setIsGeneratingCover(true); try { const cover = await aiService.generateCoverImage(storyInfo.image_prompt); if (cover) { const url = typeof cover === 'string' ? cover : URL.createObjectURL(cover); setCoverPreviewUrl(url); const saveValue = typeof cover === 'string' ? cover : await blobToBase64(cover); await dbService.updateStory(currentStoryId, { cover_image: saveValue }); } } catch (e) { alert("Vẽ lại thất bại: " + e.message); } finally { setIsGeneratingCover(false); } }; const handleStartButton = async () => { if (isProcessing) { handleStopProcessing(); return; } // Logic: If files selected, process those. Else, process all pending. let targets = []; if (selectedFiles.size > 0) { // STRICTLY SKIP COMPLETED files as requested targets = files.filter(f => selectedFiles.has(f.id) && f.status !== 'PROCESSING' && f.status !== 'COMPLETED'); } else { // Auto-detect targets: IDLE or ERROR or Raw Chars targets = files.filter(f => { if (f.status === 'IDLE' || f.status === 'ERROR' || !f.translatedContent) return true; // Check for raw content in "COMPLETED" files -- DISABLED based on user request "Do not translate again" // If user wants to fix raw chars, they should use "Manual Fix" or "Retranslate" return false; }); } if (targets.length === 0) { alert("Không có file nào cần dịch (các file đã chọn đều đã hoàn thành hoặc đang xử lý)!"); return; } setIsProcessing(true); setStartTime(Date.now()); abortControllerRef.current = new AbortController(); const signal = abortControllerRef.current.signal; // Batching Config const BATCH_SIZE = 1; // DeepSeek Context Limit let processedCount = 0; const total = targets.length; for (let i = 0; i < total; i += BATCH_SIZE) { if (signal.aborted) break; const batch = targets.slice(i, i + BATCH_SIZE); // PERSIST: Mark batch as PROCESSING in DB const processingUpdates = batch.map(f => ({ ...f, status: 'PROCESSING' })); setFiles(prev => prev.map(f => processingUpdates.find(u => u.id === f.id) || f)); await dbService.saveFilesBatch(currentStoryId, processingUpdates); // Save to DB try { const { info: results } = await aiService.translateBatch( batch, promptTemplate, additionalDictionary, storyInfo.contextNotes, signal ); // Update Results & Validate const updates = []; for (const file of batch) { let translatedText = results[file.id] || ""; // AUTO-CHECK & REPAIR const chineseRegex = /[\u4e00-\u9fa5]/; if (chineseRegex.test(translatedText)) { logger.warn(`File ${file.name} contains raw chars. Attempting repair...`); try { // Quick repair attempt translatedText = await aiService.repairTranslation(file.content, translatedText); } catch (e) { logger.error(`Repair failed for ${file.name}`, e); } } if (!translatedText) throw new Error("Empty translation"); updates.push({ id: file.id, status: 'COMPLETED', content: translatedText }); } // PERSIST: Save COMPLETED to DB immediately await dbService.saveFilesBatch(currentStoryId, updates.map(u => ({ ...batch.find(b => b.id === u.id), // merge with original to get name, etc translatedContent: u.content, status: u.status }))); setFiles(prev => prev.map(f => { const update = updates.find(u => u.id === f.id); if (update) return { ...f, status: update.status, translatedContent: update.content }; return f; })); } catch (error) { if (signal.aborted) { // Cleanup: Reset stuck processing files to IDLE in DB const resetBatch = batch.map(b => ({ ...b, status: 'IDLE' })); await dbService.saveFilesBatch(currentStoryId, resetBatch); setFiles(prev => prev.map(f => resetBatch.find(b => b.id === f.id) || f)); break; } // Error handling const errorBatch = batch.map(b => ({ ...b, status: 'ERROR', errorMessage: error.message })); await dbService.saveFilesBatch(currentStoryId, errorBatch); setFiles(prev => prev.map(f => batch.find(b => b.id === f.id) ? { ...f, status: 'ERROR', errorMessage: error.message } : f)); logger.error(`Batch failed`, error); } processedCount += batch.length; setProgressPercentage((processedCount / total) * 100); } setIsProcessing(false); setStartTime(null); abortControllerRef.current = null; logger.info("Batch translation finished"); }; const handleRetranslateMultiple = () => { if (selectedFiles.size === 0) return; setFiles(prev => prev.map(f => selectedFiles.has(f.id) ? { ...f, status: 'IDLE', translatedContent: null } : f)); }; const handleStopProcessing = () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } setIsProcessing(false); // Force reset any stuck 'PROCESSING' files to 'IDLE' setFiles(prev => prev.map(f => f.status === 'PROCESSING' ? { ...f, status: 'IDLE' } : f)); }; const handleOptimizePrompt = async () => { setIsOptimizingPrompt(true); try { const currentTmplContent = PROMPT_TEMPLATES[selectedTemplateKey]?.content || PROMPT_TEMPLATES['DEFAULT'].content; const optimized = await aiService.optimizePrompt(currentTmplContent, storyInfo, storyInfo.contextNotes); setPromptTemplate(optimized); alert("Optimized Prompt Applied!"); } catch (e) { alert("Tối ưu thất bại: " + e.message); } finally { setIsOptimizingPrompt(false); } }; const handleTemplateChange = (key) => { setSelectedTemplateKey(key); if (PROMPT_TEMPLATES[key]) setPromptTemplate(PROMPT_TEMPLATES[key].content); }; // End handleTemplateChange const handleRetranslateSingle = async (e, id) => { e.stopPropagation(); const file = files.find(f => f.id === id); if (!file) return; // Set Processing setFiles(prev => prev.map(f => f.id === id ? { ...f, status: 'PROCESSING' } : f)); await dbService.saveFilesBatch(currentStoryId, [{ ...file, status: 'PROCESSING' }]); try { const { info: results } = await aiService.translateBatch( [file], promptTemplate, additionalDictionary, storyInfo.contextNotes ); const translatedText = results[file.id]; if (!translatedText) throw new Error("Empty translation"); const updatedFile = { ...file, status: 'COMPLETED', translatedContent: translatedText }; // Update State & DB setFiles(prev => prev.map(f => f.id === id ? updatedFile : f)); await dbService.saveFilesBatch(currentStoryId, [updatedFile]); } catch (error) { // Revert to Error/Idle const errorFile = { ...file, status: 'ERROR', errorMessage: error.message }; setFiles(prev => prev.map(f => f.id === id ? errorFile : f)); await dbService.saveFilesBatch(currentStoryId, [errorFile]); alert("Lỗi dịch lại: " + error.message); } }; const handleExportEpub = async () => { if (files.length === 0) return; try { // Filter only selected if any, else all const targets = selectedFiles.size > 0 ? files.filter(f => selectedFiles.has(f.id)) : files; const blob = await epubService.generateEpub(storyInfo, targets); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${storyInfo.title || 'ebook'}.epub`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch (e) { alert("Lỗi tạo EPUB: " + e.message); console.error(e); } }; const handleDeepAnalyze = async () => { if (files.length === 0) { alert("Vui lòng tải file trước."); return; } setIsAnalyzingNames(true); setShowNameAnalysisModal(true); try { const { bible, entities } = await aiService.deepAnalyze(files, storyInfo); if (bible) setStoryInfo(prev => ({ ...prev, contextNotes: bible })); setAnalysisResults(entities || { characters: [], locations: [], terms: [] }); } catch (e) { alert("Phân tích thất bại: " + e.message); } finally { setIsAnalyzingNames(false); } }; const handleAddTerms = (terms) => { const newTerms = terms.join("\n"); setAdditionalDictionary(prev => prev ? prev + "\n" + newTerms : newTerms); alert(`Đã thêm ${terms.length} thuật ngữ!`); }; const handleFindReplace = async () => { if (!findText) return; if (!confirm(`Thay thế toàn bộ "${findText}" thành "${replaceText}" trong TẤT CẢ các file?`)) return; setFiles(prev => prev.map(f => { let content = f.content; let translated = f.translatedContent; // Replace in raw if (content && content.includes(findText)) { content = content.replaceAll(findText, replaceText); } // Replace in translation if (translated && translated.includes(findText)) { translated = translated.replaceAll(findText, replaceText); } return { ...f, content, translatedContent: translated }; })); // Save to DB? Ideally yes, but auto-save handles it on next tick or we trigger it. await dbService.updateStory(currentStoryId, { files: files }); alert("Đã thay thế xong!"); }; const handleManualCleanup = () => { if (selectedFiles.size === 0) { alert("Vui lòng chọn file để Quét rác/Định dạng."); return; } if (!confirm("Hệ thống sẽ xóa dòng rác, chuẩn hóa tiêu đề và thụt đầu dòng (Format EbookStandard). Tiếp tục?")) return; setFiles(prev => prev.map(f => { if (selectedFiles.has(f.id)) { // Cleanup RAW content const clean = formatBookStyle(f.content); return { ...f, content: clean }; } return f; })); alert("Đã định dạng xong!"); }; // --- Find & Replace Toggle --- const toggleFindReplace = () => setShowFindReplace(prev => !prev); // --- Range Logic --- const handleRangeSelect = () => { const start = parseInt(rangeStart); const end = parseInt(rangeEnd); if (isNaN(start) || isNaN(end)) { alert("Vui lòng nhập số chương hợp lệ!"); return; } const isSelect = rangeMode; // true = Select, false = Deselect const newSet = new Set(selectedFiles); let count = 0; files.forEach(f => { // Extract number from filename (e.g., "Chương 10" -> 10, "Chapter 10" -> 10) const match = f.name.match(/(\d+)/); if (match) { const num = parseInt(match[0]); if (num >= start && num <= end) { if (isSelect) { if (!newSet.has(f.id)) { newSet.add(f.id); count++; } } else { if (newSet.has(f.id)) { newSet.delete(f.id); count++; } } } } }); setSelectedFiles(newSet); alert(`Đã ${isSelect ? 'chọn' : 'bỏ chọn'} ${count} file từ chương ${start} đến ${end}.`); }; const handleRescueCopy = async (file) => { if (!file) return; try { const localDict = optimizeDictionary(additionalDictionary, file.content); const localCtx = optimizeContext(storyInfo.contextNotes, file.content); let promptContent = ""; if (PROMPT_TEMPLATES[promptTemplate]) promptContent = PROMPT_TEMPLATES[promptTemplate].content; else if (typeof promptTemplate === 'string') promptContent = promptTemplate; const rescuePrompt = `*** HỆ THỐNG CỨU HỘ DỊCH THUẬT *** Hãy dịch nội dung sau sang tiếng Việt. [NGỮ CẢNH]: ${localCtx} [TỪ ĐIỂN]: ${localDict} [YÊU CẦU]: ${promptContent} [NỘI DUNG RAW]: ${file.content}`; await navigator.clipboard.writeText(rescuePrompt); alert("Đã copy Prompt Cứu Hộ vào Clipboard! (Gồm Text + Dict + Context)"); } catch (e) { alert("Lỗi copy: " + e.message); } }; // --- Filter Logic --- const getVisibleFiles = () => { let filtered = files; if (filterStatuses.size > 0) { if (filterStatuses.has('selected')) filtered = filtered.filter(f => selectedFiles.has(f.id)); else filtered = filtered.filter(f => filterStatuses.has(f.status)); } if (currentPage > 0) { const start = (currentPage - 1) * 50; return filtered.slice(start, start + 50); } return filtered; }; const stats = { total: files.length, completed: files.filter(f => f.status === 'COMPLETED').length, processing: files.filter(f => f.status === 'PROCESSING').length, failed: files.filter(f => f.status === 'ERROR').length, pending: files.filter(f => f.status === 'IDLE').length }; const visibleFiles = getVisibleFiles(); return (
{/* Sidebar */} {/* Main Content Area */}
{!currentStoryId && (

Chào mừng trở lại!

Vui lòng chọn một truyện từ Sidebar hoặc tạo mới để bắt đầu.

AI Translate Dashboard v2.0 (SQLite Persistence)
)} alert("Changelog/Guide")} coverPreviewUrl={coverPreviewUrl} handleCoverUpload={(e) => { if (e.target.files[0]) setCoverPreviewUrl(URL.createObjectURL(e.target.files[0])); }} isGeneratingCover={isGeneratingCover} handleRegenerateCover={handleRegenerateCover} handleAutoAnalyze={handleAutoAnalyze} isAutoAnalyzing={isAutoAnalyzing} quickInput={quickInput} setQuickInput={setQuickInput} handleQuickParse={handleQuickParse} handleBackup={() => alert("Dữ liệu tự động lưu vào SQLite!")} handleRestore={() => alert("Dữ liệu tự động khôi phục từ SQLite!")} requestResetApp={() => setFiles([])} setShowGuide={() => setShowGuideModal(true)} handleContextDownload={() => alert("Download Context chưa khả dụng")} handleContextFileUpload={(e) => alert("Upload Context chưa khả dụng")} setShowNameAnalysisModal={handleDeepAnalyze} isAnalyzingNames={isAnalyzingNames} setShowSmartStartModal={() => alert("Smart Start")} dictTab={dictTab} setDictTab={setDictTab} handleDictionaryDownload={() => alert("Dict Download chưa khả dụng")} handleDictionaryUpload={() => alert("Dict Upload chưa khả dụng")} additionalDictionary={additionalDictionary} setAdditionalDictionary={setAdditionalDictionary} viewOriginalPrompt={viewOriginalPrompt} setViewOriginalPrompt={setViewOriginalPrompt} handlePromptUpload={() => alert("Prompt Upload chưa khả dụng")} resetPrompt={() => setPromptTemplate('DEFAULT')} promptTemplate={promptTemplate} setPromptTemplate={setPromptTemplate} selectedTemplateKey={selectedTemplateKey} setSelectedTemplateKey={handleTemplateChange} isOptimizingPrompt={isOptimizingPrompt} handleOptimizePrompt={handleOptimizePrompt} setShowPromptDesigner={setShowPromptDesigner} currentPage={currentPage} setCurrentPage={setCurrentPage} totalPages={Math.ceil(files.length / 50) || 1} visibleFiles={visibleFiles} selectedFiles={selectedFiles} handleSelectFile={(id, shift) => { const newSet = new Set(selectedFiles); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); setSelectedFiles(newSet); }} handleManualFixSingle={() => alert("Fix Single chưa khả dụng")} requestRetranslateSingle={() => alert("Chức năng này đang phát triển")} requestRetranslateMultiple={handleRetranslateMultiple} openEditor={(file) => { setEditorFile(file); setShowEditor(true); }} handleRemoveFile={(id) => setFiles(prev => prev.filter(f => f.id !== id))} handleFileUpload={handleFileUpload} selectAll={() => { if (selectedFiles.size === visibleFiles.length) setSelectedFiles(new Set()); else setSelectedFiles(new Set(visibleFiles.map(f => f.id))); }} rangeStart={rangeStart} setRangeStart={setRangeStart} rangeEnd={rangeEnd} setRangeEnd={setRangeEnd} isProcessing={isProcessing} runSmartAutomation={() => alert("Smart Automation đang phát triển")} showFilterPanel={showFilterPanel} setShowFilterPanel={setShowFilterPanel} filterStatuses={filterStatuses} toggleFilterStatus={(status) => { const newSet = new Set(filterStatuses); if (newSet.has(status)) newSet.delete(status); else newSet.add(status); setFilterStatuses(newSet); }} handleManualCleanup={handleManualCleanup} handleSmartDelete={() => { if (confirm(`Xóa ${selectedFiles.size} file đã chọn?`)) { setFiles(prev => prev.filter(f => !selectedFiles.has(f.id))); setSelectedFiles(new Set()); } }} requestDeleteAll={() => { if (confirm("Xóa TẤT CẢ danh sách?")) setFiles([]); }} handleDownloadMerged={() => alert("Tính năng Gộp chưa khả dụng")} handleStartButton={handleStartButton} stopProcessing={handleStopProcessing} // Find & Replace Props showFindReplace={showFindReplace} setShowFindReplace={setShowFindReplace} toggleFindReplace={toggleFindReplace} findText={findText} setFindText={setFindText} replaceText={replaceText} setReplaceText={setReplaceText} handleFindReplace={handleFindReplace} // Range Props handleRangeSelect={handleRangeSelect} rangeStart={rangeStart} setRangeStart={setRangeStart} rangeEnd={rangeEnd} setRangeEnd={setRangeEnd} rangeMode={rangeMode} setRangeMode={setRangeMode} rangeMode={rangeMode} setRangeMode={setRangeMode} handleExportEpub={handleExportEpub} requestRetranslateSingle={handleRetranslateSingle} handleTestModel={() => { }} enabledModels={[]} modelConfigs={[]} modelUsages={{}} />
{showSplitter && pendingSplitFile && ( { setShowSplitter(false); setPendingSplitFile(null); }} onConfirmSplit={handleSplitConfirm} /> )} {showEditor && editorFile && ( { setShowEditor(false); setEditorFile(null); }} onSave={(id, raw, translated) => { setFiles(prev => prev.map(f => f.id === id ? { ...f, content: raw, translatedContent: translated } : f)); }} handleRescueCopy={() => handleRescueCopy(editorFile)} /> )} {showGuideModal && setShowGuideModal(false)} />} setStoryToDelete(null)} onConfirm={confirmDeleteStory} title="Xóa Truyện?" message={`Bạn có chắc muốn xóa truyện "${storyToDelete?.title || 'Chưa đặt tên'}" không? Hành động này không thể hoàn tác.`} confirmText="Xóa Vĩnh Viễn" /> {showNameAnalysisModal && setShowNameAnalysisModal(false)} isAnalyzing={isAnalyzingNames} analysisResults={analysisResults} onAddTerms={handleAddTerms} />} {showLogs && setShowLogs(false)} />}
); } export default App;