Files
ai-translate/src/App.jsx
T
2026-01-12 12:08:04 +07:00

864 lines
33 KiB
React

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 (
<div className={`h-screen w-full overflow-hidden flex ${isDarkMode ? 'dark' : ''}`}>
{/* Sidebar */}
<Sidebar
stories={stories}
currentStoryId={currentStoryId}
onSelectStory={handleSelectStory}
onCreateStory={handleCreateStory}
onDeleteStory={handleDeleteStory}
onUpdateStory={handleUpdateStory}
isOpen={isSidebarOpen}
setIsOpen={setIsSidebarOpen}
/>
{/* Main Content Area */}
<div className="flex-1 h-full min-w-0 flex flex-col relative transition-all duration-300">
{!currentStoryId && (
<div className="absolute inset-0 z-50 bg-slate-50 dark:bg-[#0d1117] flex flex-col items-center justify-center p-10 text-center animate-in fade-in">
<div className="bg-white dark:bg-[#161b22] p-8 rounded-3xl shadow-2xl max-w-md w-full border border-slate-100 dark:border-[#30363d]">
<h2 className="text-2xl font-bold text-slate-800 dark:text-gray-100 mb-4">Chào mừng trở lại!</h2>
<p className="text-slate-500 mb-8">Vui lòng chọn một truyện từ Sidebar hoặc tạo mới để bắt đầu.</p>
<button onClick={handleCreateStory} className="w-full py-3 bg-indigo-600 hover:bg-indigo-500 text-white font-bold rounded-xl transition-all shadow-lg shadow-indigo-200/50">
+ Tạo Truyện Mới
</button>
<div className="mt-8 pt-8 border-t border-slate-100 dark:border-[#30363d] text-xs text-slate-400">
AI Translate Dashboard v2.0 (SQLite Persistence)
</div>
</div>
</div>
)}
<MainUI
isDarkMode={isDarkMode}
toggleTheme={toggleTheme}
files={files}
stats={stats}
progressPercentage={progressPercentage}
storyInfo={storyInfo}
setStoryInfo={setStoryInfo}
showSettings={showSettings}
setShowSettings={setShowSettings}
showLogs={showLogs}
setShowLogs={setShowLogs}
onShowChangelog={() => 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={{}}
/>
</div>
{showSplitter && pendingSplitFile && (
<SplitterModal
isOpen={showSplitter}
fileName={pendingSplitFile.name}
fileContent={pendingSplitFile.content}
onCancel={() => { setShowSplitter(false); setPendingSplitFile(null); }}
onConfirmSplit={handleSplitConfirm}
/>
)}
{showEditor && editorFile && (
<EditorModal
isOpen={showEditor}
file={editorFile}
onClose={() => { 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 && <GuideModal isOpen={showGuideModal} onClose={() => setShowGuideModal(false)} />}
<ConfirmationModal
isOpen={!!storyToDelete}
onClose={() => 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 && <NameAnalysisModal
isOpen={showNameAnalysisModal}
onClose={() => setShowNameAnalysisModal(false)}
isAnalyzing={isAnalyzingNames}
analysisResults={analysisResults}
onAddTerms={handleAddTerms}
/>}
{showLogs && <LogModal isOpen={showLogs} onClose={() => setShowLogs(false)} />}
</div>
);
}
export default App;