diff --git a/src/features/script-tokens/ScriptTokensPage.tsx b/src/features/script-tokens/ScriptTokensPage.tsx index ace66b0..75d4c93 100644 --- a/src/features/script-tokens/ScriptTokensPage.tsx +++ b/src/features/script-tokens/ScriptTokensPage.tsx @@ -46,16 +46,126 @@ function getGrade(score: number): string { } const HISTORY_KEY = "omniai:script-eval-history"; +const TEXT_FILE_EXTENSIONS = [ + ".txt", + ".text", + ".md", + ".markdown", + ".fountain", + ".fdx", + ".rtf", + ".csv", + ".tsv", + ".json", + ".jsonl", + ".xml", + ".html", + ".htm", + ".yaml", + ".yml", + ".toml", + ".ini", + ".conf", + ".cfg", + ".properties", + ".log", + ".srt", + ".ass", + ".ssa", + ".vtt", + ".sql", + ".js", + ".jsx", + ".ts", + ".tsx", + ".py", + ".java", + ".c", + ".cpp", + ".h", + ".hpp", + ".cs", + ".go", + ".rs", + ".php", + ".rb", + ".sh", + ".bat", + ".ps1", + ".lua", + ".swift", + ".kt", + ".kts", +] as const; +const TEXT_FILE_EXTENSION_SET = new Set(TEXT_FILE_EXTENSIONS); +const TEXT_FILE_ACCEPT = TEXT_FILE_EXTENSIONS.join(","); +const TEXT_FILE_HINT = "支持常见文本格式:TXT / MD / Fountain / FDX / RTF / JSON / CSV / XML / HTML / YAML / LOG / 字幕等"; function loadHistory(): HistoryEntry[] { try { const raw = localStorage.getItem(HISTORY_KEY); - return raw ? (JSON.parse(raw) as HistoryEntry[]) : []; + return raw ? (JSON.parse(raw) as HistoryEntry[]).sort((a, b) => b.timestamp - a.timestamp) : []; } catch { return []; } } function saveHistory(entries: HistoryEntry[]) { - try { localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.slice(0, 20))); } catch { /* quota exceeded */ } + try { localStorage.setItem(HISTORY_KEY, JSON.stringify(entries.sort((a, b) => b.timestamp - a.timestamp).slice(0, 20))); } catch { /* quota exceeded */ } +} + +function getFileExtension(filename: string): string { + const dotIndex = filename.lastIndexOf("."); + return dotIndex >= 0 ? filename.slice(dotIndex).toLowerCase() : ""; +} + +function isReadableTextFile(file: File, ext: string): boolean { + const mime = file.type.toLowerCase(); + return ( + TEXT_FILE_EXTENSION_SET.has(ext) || + mime.startsWith("text/") || + mime === "application/json" || + mime === "application/xml" || + mime === "application/xhtml+xml" || + mime === "application/x-yaml" || + mime === "application/yaml" + ); +} + +async function decodeTextFile(file: File): Promise { + const bytes = await file.arrayBuffer(); + const utf8 = new TextDecoder("utf-8", { fatal: false }).decode(bytes); + if (!utf8.includes("\uFFFD")) return utf8; + + try { + return new TextDecoder("gb18030", { fatal: false }).decode(bytes); + } catch { + return utf8; + } +} + +function normalizeUploadedText(raw: string, ext: string): string { + if (ext === ".rtf") { + const text = raw + .replace(/\\par[d]?/gi, "\n") + .replace(/\\line/gi, "\n") + .replace(/\\'[0-9a-f]{2}/gi, "") + .replace(/\\[a-z]+\d* ?/gi, "") + .replace(/[{}]/g, "") + .replace(/\n{3,}/g, "\n\n") + .trim(); + return text || raw; + } + + if ([".html", ".htm", ".xml", ".fdx"].includes(ext) && typeof DOMParser !== "undefined") { + try { + const doc = new DOMParser().parseFromString(raw, ext === ".html" || ext === ".htm" ? "text/html" : "application/xml"); + const text = doc.documentElement.textContent?.replace(/\n{3,}/g, "\n\n").trim(); + return text || raw; + } catch { + return raw; + } + } + + return raw; } const SCORE_DIMENSIONS: ScoreDimension[] = [ @@ -138,13 +248,14 @@ function ScriptTokensPage() { const handleFileUpload = async (event: ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; - const ext = file.name.slice(file.name.lastIndexOf(".")).toLowerCase(); - const readable = [".txt", ".md"].includes(ext) || file.type === "text/plain" || file.type === "text/markdown"; + const ext = getFileExtension(file.name); + const readable = isReadableTextFile(file, ext); setUploadedFile({ name: file.name, size: file.size }); if (readable) { - setScript(await file.text()); + const text = normalizeUploadedText(await decodeTextFile(file), ext); + setScript(text); } else { - setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext.toUpperCase()} 格式,请上传 TXT 或 MD 文件。`); + setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext ? ext.toUpperCase() : "未知"} 格式,请上传常见文本类文件。`); } event.target.value = ""; }; @@ -167,7 +278,9 @@ function ScriptTokensPage() { score: aiResult.totalScore, grade: g, }; - const updated = [entry, ...loadHistory().filter((h) => h.name !== entry.name || h.score !== entry.score)]; + const updated = [entry, ...loadHistory().filter((h) => h.name !== entry.name || h.score !== entry.score)].sort( + (a, b) => b.timestamp - a.timestamp, + ); saveHistory(updated); setHistory(updated); } catch (err) { @@ -231,6 +344,8 @@ function ScriptTokensPage() { const grade = result ? getGrade(result.totalScore) : null; const beatPct = result ? (result.totalScore >= 95 ? 97 : result.totalScore >= 88 ? 92 : result.totalScore >= 80 ? 85 : 72) : 0; const compactTitle = uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? "剧本评测"; + const scriptMinutes = Math.max(8, Math.round(script.length / 460)); + const reportDate = new Date().toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit" }); return (
@@ -261,11 +376,11 @@ function ScriptTokensPage() { -
支持 .txt .md
+
{TEXT_FILE_HINT}
)} - +
@@ -359,45 +474,31 @@ function ScriptTokensPage() {
-
+
{!result && (
- {/* Script-themed upload illustration */} -
fileInputRef.current?.click()} - onKeyDown={uploadKeyDown} - > -
- {[0, 1, 2, 3, 4, 5].map((idx) => ( -
-
-
-
-
-
-
-
-
- ))} +
+
fileInputRef.current?.click()} + onKeyDown={uploadKeyDown} + > +
+ +
+
+ {uploadedFile ? "剧本已导入" : "上传剧本文件"} +
+
+ {uploadedFile + ? "如需更换,点击此处重新上传;完成后点击左侧开始评测。" + : `${TEXT_FILE_HINT},上传后点击开始评测,AI 将识别剧本信息。`} +
-
- - 上传或粘贴剧本开始评测 -
-
支持 TXT / MD 格式,点击此处或左侧上传区导入剧本文件
-
-