Files
omniai-web/src/features/script-tokens/ScriptTokensPage.tsx
T
ludan b08a7918da feat: 剧本评分左侧面板滚动优化、电商克隆移动端适配、视觉细节精修
【剧本评分左侧面板滚动重构】
- 新增 script-eval-v5-left-main 滚动容器,上传区/AI信息/历史记录统一在容器内滚动
- 底部操作按钮(开始评测/导出报告)独立于滚动区外,始终可见可点击
- 历史评测列表增加 max-height 限制,超出区域内置滚动条
- 自定义窄滚动条(品牌绿半透明 thumb),保持视觉干净
- 短视口(≤760px/820px)压缩上传区和历史列表最小高度

【剧本评分视觉精修】
- 左侧面板增加渐变背景层次与分区微光分割线
- 上传区增加 ::after 伪元素径向光晕,hover 时品牌绿边框增强
- 已上传状态上传区增加绿色边框高亮(is-ready/is-complete)
- 底部操作栏背景层次加深,导出按钮 hover 增加绿色反馈
- 右侧面板增加底部径向渐变,上传引导卡标题提亮
- 顶部状态栏背景加深,模糊效果增强

【电商克隆移动端适配增强】
- 900px/620px/480px 三级断点增加顶部预留空间,避免与导航重叠
- Logo 区域定位从 sticky 改为 static,避免滚动时遮挡内容
- 设置面板在窄屏下调整内边距与边距

【Token 用量页精简】
- 移除指标卡片序号角标,保持卡片视觉简洁
2026-06-04 09:40:28 +08:00

704 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
BarChartOutlined,
CheckCircleFilled,
CopyOutlined,
DownloadOutlined,
FileTextOutlined,
LoadingOutlined,
ThunderboltOutlined,
UploadOutlined,
} from "@ant-design/icons";
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { evaluateScript } from "../../api/scriptEvalClient";
import { useSessionStore } from "../../stores";
interface ScoreDimension {
key: string;
label: string;
maxScore: number;
hint: string;
detail: string;
}
interface EvalResult {
totalScore: number;
grade: string;
dimensionScores: Record<string, number>;
summary: string;
issues: string[];
highlights: string[];
suggestions: string[];
}
interface HistoryEntry {
name: string;
date: string;
timestamp: number;
score: number;
grade: string;
}
function getGrade(score: number): string {
if (score >= 97) return "S+";
if (score >= 93) return "S";
if (score >= 88) return "A+";
if (score >= 83) return "A";
if (score >= 78) return "B+";
if (score >= 70) return "B";
return "C";
}
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<string>(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[]).sort((a, b) => b.timestamp - a.timestamp) : [];
} catch { return []; }
}
function saveHistory(entries: HistoryEntry[]) {
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<string> {
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;
}
function formatFileSize(size: number): string {
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
return `${(size / 1024 / 1024).toFixed(1)} MB`;
}
const SCORE_DIMENSIONS: ScoreDimension[] = [
{ key: "hook", label: "钩子设计", maxScore: 20, hint: "开篇吸引力·悬念设置·黄金三秒", detail: "开篇即抛出高概念钩子,悬念设置紧凑有力。" },
{ key: "character", label: "角色塑造", maxScore: 15, hint: "人物立体度·动机合理性·弧光设计", detail: "主角动机有铺垫,配角功能性较强,人物弧光尚可进一步深化。" },
{ key: "plot", label: "剧情结构", maxScore: 20, hint: "起承转合·节奏把控·冲突设计", detail: "起承转合完整,节奏把控稳健,冲突设计有张力。" },
{ key: "logic", label: "逻辑严密", maxScore: 15, hint: "世界观自洽·伏笔回收·因果链", detail: "世界观整体自洽,伏笔设置到位。" },
{ key: "visual", label: "场景构建", maxScore: 15, hint: "空间描写·视听语言·画面想象力", detail: "视觉意象统一而强烈,场景描写极具画面感。" },
{ key: "content", label: "内容深度", maxScore: 15, hint: "主题表达·情感共鸣·思想内核", detail: "核心设定将科技伦理与人性困境紧密结合,主题表达深刻有力。" },
];
function formatReportMarkdown(result: EvalResult, script: string): string {
const lines: string[] = [];
lines.push(`# 剧本评测报告`);
lines.push("");
lines.push(`**综合评分**: ${result.totalScore} / 100 · ${result.grade}`);
lines.push("");
lines.push(`## 概要`);
lines.push(result.summary);
lines.push("");
lines.push(`## 六维评分`);
for (const dim of SCORE_DIMENSIONS) {
const score = result.dimensionScores[dim.key] ?? 0;
const pct = Math.round((score / dim.maxScore) * 100);
lines.push(`- **${dim.label}**: ${score}/${dim.maxScore} (${pct}%) — ${dim.hint}`);
}
if (result.highlights.length > 0) {
lines.push("");
lines.push(`## 亮点`);
result.highlights.forEach((h) => lines.push(`- ${h}`));
}
if (result.issues.length > 0) {
lines.push("");
lines.push(`## 扣分点`);
result.issues.forEach((i) => lines.push(`- ${i}`));
}
if (result.suggestions.length > 0) {
lines.push("");
lines.push(`## 优化建议`);
result.suggestions.forEach((s) => lines.push(`- ${s}`));
}
lines.push("");
lines.push(`---`);
lines.push(`*评测时间: ${new Date().toLocaleString("zh-CN")}*`);
return lines.join("\n");
}
function ScriptTokensPage() {
const [script, setScript] = useState("");
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<EvalResult | null>(null);
const [evalError, setEvalError] = useState<string | null>(null);
const [uploadedFile, setUploadedFile] = useState<{ name: string; size: number } | null>(null);
const [copied, setCopied] = useState(false);
const [activeDim, setActiveDim] = useState<number | null>(null);
const [animatedScore, setAnimatedScore] = useState(0);
const [history, setHistory] = useState<HistoryEntry[]>(loadHistory);
const fileInputRef = useRef<HTMLInputElement>(null);
const scoreFrameRef = useRef<number | null>(null);
const session = useSessionStore((s) => s.session);
const hasContent = Boolean(script.trim());
// Score animation
useEffect(() => {
if (!result) return;
const start = performance.now();
const target = result.totalScore;
const dur = 1400;
function tick(now: number) {
const t = Math.min((now - start) / dur, 1);
const e = 1 - Math.pow(1 - t, 3);
setAnimatedScore(Math.round(e * target));
if (t < 1) scoreFrameRef.current = requestAnimationFrame(tick);
}
scoreFrameRef.current = requestAnimationFrame(tick);
return () => { if (scoreFrameRef.current) cancelAnimationFrame(scoreFrameRef.current); };
}, [result]);
const handleFileUpload = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
const ext = getFileExtension(file.name);
const readable = isReadableTextFile(file, ext);
setUploadedFile({ name: file.name, size: file.size });
if (readable) {
const text = normalizeUploadedText(await decodeTextFile(file), ext);
setScript(text);
} else {
setScript(`[已上传文件:${file.name}]\n\n暂不支持解析 ${ext ? ext.toUpperCase() : "未知"} 格式,请上传常见文本类文件。`);
}
event.target.value = "";
};
const handleEvaluate = async () => {
if (!hasContent) return;
setLoading(true);
setResult(null);
setEvalError(null);
setAnimatedScore(0);
setActiveDim(null);
try {
const aiResult = await evaluateScript(script);
setResult(aiResult);
const g = getGrade(aiResult.totalScore);
const entry: HistoryEntry = {
name: uploadedFile?.name?.replace(/\.[^.]+$/, "") ?? `剧本 ${new Date().toLocaleDateString("zh-CN")}`,
date: new Date().toLocaleDateString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }),
timestamp: Date.now(),
score: aiResult.totalScore,
grade: g,
};
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) {
setEvalError(err instanceof Error ? err.message : "评测服务暂时不可用,请稍后重试");
}
setLoading(false);
};
const handleReset = () => {
setScript("");
setResult(null);
setEvalError(null);
setUploadedFile(null);
setCopied(false);
setAnimatedScore(0);
setActiveDim(null);
if (fileInputRef.current) fileInputRef.current.value = "";
};
const handleCopyReport = async () => {
if (!result) return;
const text = formatReportMarkdown(result, script);
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
const handleExportMarkdown = () => {
if (!result) return;
const md = formatReportMarkdown(result, script);
const blob = new Blob([md], { type: "text/markdown;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `剧本评测_${result.grade}级_${result.totalScore}分_${new Date().toISOString().slice(0, 10)}.md`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const uploadKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
fileInputRef.current?.click();
};
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" });
const statusClass = loading ? "is-loading" : result ? "is-complete" : hasContent ? "is-ready" : "is-idle";
return (
<section className={`script-eval-v5 page-motion ${statusClass}`}>
<div className="script-eval-v5-page">
{/* Left Panel */}
<aside className="script-eval-v5-left">
<div className="script-eval-v5-left-main">
<div className="script-eval-v5-lp-section">
<div className="script-eval-v5-lp-label"></div>
<div
className="script-eval-v5-upload-zone"
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()}
onKeyDown={uploadKeyDown}
>
{uploadedFile ? (
<div className="script-eval-v5-upload-done is-show">
<CheckCircleFilled />
<span className="script-eval-v5-uf-meta">
<span className="script-eval-v5-uf-name">{uploadedFile.name}</span>
<span className="script-eval-v5-uf-size">{formatFileSize(uploadedFile.size)}</span>
</span>
<span className="script-eval-v5-uf-re" onClick={(e) => { e.stopPropagation(); handleReset(); }}>
</span>
</div>
) : (
<>
<div className="script-eval-v5-upload-icon"><UploadOutlined /></div>
<div className="script-eval-v5-upload-text"></div>
<button type="button" className="script-eval-v5-upload-btn" onClick={(e) => { e.stopPropagation(); fileInputRef.current?.click(); }}>
<UploadOutlined />
</button>
<div className="script-eval-v5-upload-hint">{TEXT_FILE_HINT}</div>
</>
)}
</div>
<input ref={fileInputRef} type="file" accept={TEXT_FILE_ACCEPT} style={{ display: "none" }} onChange={handleFileUpload} />
</div>
<div className="script-eval-v5-lp-section">
<div className="script-eval-v5-lp-label">AI </div>
<div className="script-eval-v5-info-grid">
{!result ? (
<div className="script-eval-v5-info-empty"></div>
) : (
<>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val"><span className="script-eval-v5-info-tag">{result.totalScore} · {grade}</span></span>
</div>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val">{script.length} </span>
</div>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val">{new Date().toLocaleDateString("zh-CN")}</span>
</div>
<div className="script-eval-v5-info-item">
<span className="script-eval-v5-info-key"></span>
<span className="script-eval-v5-info-val">{beatPct}%</span>
</div>
</>
)}
</div>
</div>
<div className="script-eval-v5-lp-section is-fill">
<div className="script-eval-v5-lp-label"></div>
<div className="script-eval-v5-history-list">
{!session ? (
<div className="script-eval-v5-history-empty"></div>
) : history.length === 0 ? (
<div className="script-eval-v5-history-empty"></div>
) : (
history.map((item, i) => (
<div key={i} className={`script-eval-v5-history-item${i === 0 ? " is-active" : ""}`}>
<div className="script-eval-v5-hi-left">
<div className="script-eval-v5-hi-name">{item.name}</div>
<div className="script-eval-v5-hi-date">{item.date}</div>
<div className="script-eval-v5-hi-bar">
<div className="script-eval-v5-hi-bar-fill" style={{ width: `${Math.min(92, (item.score / 100) * 100)}%` }} />
</div>
</div>
<div className="script-eval-v5-hi-right">
<div className={`script-eval-v5-hi-score${item.score >= 90 ? " is-green" : ""}`}>{item.score}</div>
<div className="script-eval-v5-hi-grade">{item.grade}</div>
</div>
</div>
))
)}
</div>
</div>
<div className="script-eval-v5-lp-bottom">
<button
type="button"
className="script-eval-v5-eval-btn"
disabled={loading || !hasContent}
onClick={() => void handleEvaluate()}
>
{loading ? <LoadingOutlined /> : <ThunderboltOutlined />}
<span>{loading ? "评测中..." : "开始评测"}</span>
</button>
<button type="button" className="script-eval-v5-export-btn" disabled={!result} onClick={handleExportMarkdown}>
<DownloadOutlined />
<span></span>
</button>
</div>
</div>
</aside>
{/* Right Area */}
<div className="script-eval-v5-right">
<div className="script-eval-v5-right-topbar">
<div className="script-eval-v5-right-title">
<span className="script-eval-v5-rt-green"></span>
{uploadedFile && <> · {compactTitle}</>}
</div>
<div className="script-eval-v5-right-actions">
{result && (
<>
<button type="button" className="script-eval-v5-action-btn" onClick={() => void handleCopyReport()}>
<CopyOutlined />{copied ? "已复制" : "复制"}
</button>
<button type="button" className="script-eval-v5-action-btn" onClick={handleExportMarkdown}>
<DownloadOutlined />
</button>
</>
)}
</div>
</div>
<div className={`script-eval-v5-right-content${result ? " is-report" : ""}`}>
{loading ? (
<div className="script-eval-v5-input-section">
<div className="script-eval-v5-illustration" aria-label="评测中">
<div className="script-eval-v5-loading">
<div className="page-loading-spinner" />
<strong>AI ...</strong>
<p> 15-30 </p>
<div className="script-eval-v5-loading-steps" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
) : !result && (
<div className="script-eval-v5-input-section">
<div className="script-eval-v5-illustration" aria-label="上传剧本并开始评测">
<div
className="script-eval-v5-illustration-hit"
role="button"
tabIndex={0}
onClick={() => fileInputRef.current?.click()}
onKeyDown={uploadKeyDown}
>
<div className="script-eval-v5-upload-card-icon">
<FileTextOutlined />
</div>
<div className="script-eval-v5-upload-card-title">
{uploadedFile ? "剧本已导入" : "上传剧本文件"}
</div>
<div className="script-eval-v5-upload-card-desc">
{uploadedFile
? "如需更换,点击此处重新上传;完成后点击左侧开始评测。"
: `${TEXT_FILE_HINT},上传后点击开始评测,AI 将识别剧本信息。`}
</div>
</div>
</div>
{evalError && (
<div className="script-eval-v5-error" role="alert">
<span></span><span>{evalError}</span>
<button type="button" className="script-eval-v5-retry-btn" onClick={() => void handleEvaluate()} disabled={!hasContent}>
</button>
</div>
)}
</div>
)}
{result && (
<section className="script-eval-report script-eval-report--inside">
<div className="script-eval-report__body">
<header className="script-eval-report__hero">
<div className="script-eval-report__score-block">
<div className="script-eval-report__score-row">
<span className="script-eval-report__score">{animatedScore}</span>
<span className="script-eval-report__score-total">/ 100</span>
<span className="script-eval-report__grade">
<i />
{grade}
</span>
</div>
<div className="script-eval-report__score-line">
<span style={{ width: `${animatedScore}%` }} />
</div>
<div className="script-eval-report__beat">{result.totalScore >= 90 ? "优秀" : result.totalScore >= 80 ? "良好" : result.totalScore >= 70 ? "中等" : "待提升"}{result.totalScore >= 85 ? "具备商业开发潜力" : "建议针对性优化后再提交"}</div>
</div>
<div className="script-eval-report__summary">
<div className="script-eval-report__title-line">
<div>
<h1>{compactTitle}</h1>
<p>{`剧本评测 · ${scriptMinutes} min · ${reportDate}`}</p>
</div>
</div>
<p className="script-eval-report__desc">{result.summary}</p>
</div>
</header>
<section className="script-eval-report__chart-card" aria-label="维度拆解">
<div className="script-eval-report__card-head">
<span><i /></span>
<div className="script-eval-report__legend">
<span><i className="is-score" /></span>
<span><i className="is-loss" /></span>
</div>
</div>
<div className="script-eval-report__chart">
<div className="script-eval-report__axis">
<span>100%</span>
<span>80%</span>
<span>60%</span>
<span>40%</span>
<span>20%</span>
<span>0%</span>
</div>
<div className="script-eval-report__chart-grid">
{SCORE_DIMENSIONS.map((dim, dimIndex) => {
const score = result.dimensionScores[dim.key] ?? 0;
const pct = Math.max(0, Math.min(1, score / dim.maxScore));
const lossPct = 1 - pct;
const isPerfect = score === dim.maxScore;
const isActive = activeDim === null || activeDim === dimIndex;
return (
<button
key={dim.key}
type="button"
className={`script-eval-report__bar-col${isActive ? "" : " is-dimmed"}`}
onMouseEnter={() => setActiveDim(dimIndex)}
onFocus={() => setActiveDim(dimIndex)}
onMouseLeave={() => setActiveDim(null)}
onBlur={() => setActiveDim(null)}
aria-label={`${dim.label} ${score}/${dim.maxScore}${dim.hint}`}
>
<div className="script-eval-report__bar-score">
<b>{score}</b><small>/{dim.maxScore}</small>{isPerfect ? <em>*</em> : null}
</div>
<div className="script-eval-report__bar-box">
{lossPct > 0 ? <div className="script-eval-report__bar-loss" style={{ height: `${lossPct * 100}%` }} /> : null}
<div className="script-eval-report__bar-fill" style={{ height: `${pct * 100}%` }} />
</div>
<strong>{dim.label}</strong>
<span>{dim.hint}</span>
</button>
);
})}
</div>
</div>
<div className="script-eval-report__chart-note">
<BarChartOutlined />
<span>
{activeDim === null
? "悬停维度可查看当前分项表现,优先从低分项制定改稿计划。"
: `${SCORE_DIMENSIONS[activeDim].label}${SCORE_DIMENSIONS[activeDim].detail}`}
</span>
</div>
</section>
<div className="script-eval-report__findings">
{result.highlights.length > 0 ? (
<section className="script-eval-report__finding-group is-highlight">
<h2> <span>{result.highlights.length}</span></h2>
<div>
{result.highlights.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
</section>
) : null}
{result.issues.length > 0 ? (
<section className="script-eval-report__finding-group is-issue">
<h2> <span>{result.issues.length}</span></h2>
<div>
{result.issues.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
</section>
) : null}
</div>
{result.suggestions.length > 0 ? (
<section className="script-eval-report__path-card">
<div className="script-eval-report__card-head">
<span><i /></span>
</div>
<table className="script-eval-report__path-table">
<thead>
<tr>
<th>#</th>
<th></th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{result.suggestions.map((item, index) => {
const high = index < 2;
return (
<tr key={index}>
<td>{String(index + 1).padStart(2, "0")}</td>
<td>{high ? "核心" : "增强"}</td>
<td>{item}</td>
<td><span className={high ? "is-high" : "is-mid"}>{high ? "HIGH" : "MID"}</span></td>
</tr>
);
})}
</tbody>
</table>
</section>
) : null}
</div>
</section>
)}
</div>
<div className="script-eval-v5-statusbar">
<div className="script-eval-v5-status-dot" />
<span>{loading ? "评测中..." : result ? "评测完成" : hasContent ? "待评测" : "等待上传"}</span>
<span className="script-eval-v5-sb-right">{result ? `六维标准 · ${result.totalScore}` : "六维标准"}</span>
</div>
</div>
</div>
</section>
);
}
export default ScriptTokensPage;