<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-lang-key="title">在线成绩分析平台</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<!-- PDF.js Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.worker.min.js';
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
/* Lock scroll when not logged in */
body.locked { overflow: hidden; height: 100vh; }
.nav-link.active {
background-color: #eff6ff;
color: #1d4ed8;
font-weight: 600;
}
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #f1f5f9; }
::-webkit-scrollbar-thumb { background: #94a3b8; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #64748b; }
.ai-loader {
width: 24px;
height: 24px;
border: 3px solid #3B82F6;
border-bottom-color: transparent;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.file-drop-area {
border: 2px dashed #d1d5db;
border-radius: 0.5rem;
padding: 1.5rem;
text-align: center;
background-color: #f9fafb;
transition: background-color 0.2s, border-color 0.2s;
}
.file-drop-area.dragover {
background-color: #eff6ff;
border-color: #3b82f6;
}
.report-section ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-top: 0.5rem;
}
/* Ensure login covers everything */
#login-page {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
overflow-y: auto;
background-color: #e2e8f0; /* bg-slate-200 */
}
</style>
</head>
<body class="bg-slate-100 locked">
<!-- Login Page -->
<div id="login-page" class="flex items-center justify-center">
<div class="w-full max-w-md p-8 space-y-6 bg-white rounded-xl shadow-lg m-4">
<div class="text-center">
<h1 data-lang-key="login_title" class="text-3xl font-bold text-slate-800">在线成绩分析平台</h1>
<p data-lang-key="login_subtitle" class="mt-2 text-slate-600">请输入访问密码</p>
<p class="text-xs text-slate-500 mt-1">验证将在开始分析时进行</p>
</div>
<form id="login-form" class="space-y-6">
<div>
<input type="password" id="password" required class="w-full px-4 py-2 text-lg text-center bg-slate-100 border-2 border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition" placeholder="••••••••">
</div>
<button type="submit" data-lang-key="login_button" class="w-full px-4 py-3 font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition">进入系统</button>
</form>
</div>
</div>
<!-- Main Application -->
<div id="app" class="hidden h-screen w-full lg:grid lg:grid-cols-[280px_1fr]">
<!-- Sidebar Navigation -->
<div class="hidden border-r bg-white lg:block">
<div class="flex h-full max-h-screen flex-col gap-2">
<div class="flex h-14 items-center border-b px-6">
<a href="#" class="flex items-center gap-2 font-semibold">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600"><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path><rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect></svg>
<span data-lang-key="app_title" class="text-lg">成绩分析</span>
</a>
</div>
<div class="flex-1 overflow-auto py-2">
<nav class="grid items-start px-4 text-sm font-medium">
<a href="#grade-entry" class="nav-link flex items-center gap-3 rounded-lg px-3 py-3 text-slate-600 transition-all hover:bg-slate-100 active">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5"><path d="M12 22h6a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v5"></path><path d="M14 2v4a2 2 0 0 0 2 2h4"></path><path d="M3 15h6"></path><path d="M6 12v6"></path></svg>
<span data-lang-key="nav_grade_entry">成绩录入 (简易)</span>
</a>
<a href="#test-paper-analysis" class="nav-link flex items-center gap-3 rounded-lg px-3 py-3 text-slate-600 transition-all hover:bg-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-5 w-5"><path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/><path d="M5 3v4"/><path d="M19 17v4"/><path d="M3 5h4"/><path d="M17 19h4"/></svg>
<span data-lang-key="nav_test_paper_analysis">试卷分析 (综合)</span>
</a>
</nav>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex flex-col">
<header class="flex h-14 items-center gap-4 border-b bg-white px-6">
<h1 id="page-title" data-lang-key="page_title_grade_entry" class="text-xl font-semibold">成绩录入 (简易)</h1>
<div class="ml-auto flex items-center gap-2">
<div class="flex items-center text-sm font-medium bg-slate-200 rounded-lg p-0.5">
<button id="lang-en" class="px-3 py-1 rounded-md text-slate-600 transition-all">EN</button>
<button id="lang-zh" class="px-3 py-1 rounded-md text-slate-600 transition-all bg-white text-blue-600 shadow-sm">ZH</button>
</div>
</div>
</header>
<main class="flex-1 overflow-y-auto p-6 space-y-6">
<!-- Dashboard Section -->
<div id="dashboard-section" class="page-content space-y-6">
<div id="welcome-message" class="text-center py-16 px-6 bg-white rounded-xl shadow">
<h2 data-lang-key="welcome_title" class="text-2xl font-bold text-slate-800">欢迎使用成绩分析平台</h2>
<p data-lang-key="welcome_text" class="mt-2 text-slate-600 max-w-2xl mx-auto">请在下方“成绩录入”板块输入或上传学生分数以生成简易报告。</p>
<p data-lang-key="welcome_text_2" class="mt-1 text-slate-600 max-w-2xl mx-auto">如需多文件综合分析,请使用“试卷分析”功能。</p>
</div>
<div id="report-container" class="hidden space-y-6">
<!-- Stat Cards & Charts -->
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-5">
<div class="p-5 bg-white rounded-xl shadow"><h3 data-lang-key="stat_students" class="text-sm font-medium text-slate-500">学生人数</h3><p id="stat-students" class="text-3xl font-bold text-slate-800">0</p></div>
<div class="p-5 bg-white rounded-xl shadow"><h3 data-lang-key="stat_avg" class="text-sm font-medium text-slate-500">平均分</h3><p id="stat-avg" class="text-3xl font-bold text-slate-800">0.00</p></div>
<div class="p-5 bg-white rounded-xl shadow"><h3 data-lang-key="stat_stddev" class="text-sm font-medium text-slate-500">标准差</h3><p id="stat-stddev" class="text-3xl font-bold text-slate-800">0.00</p></div>
<div class="p-5 bg-white rounded-xl shadow"><h3 data-lang-key="stat_highest" class="text-sm font-medium text-slate-500">最高分</h3><p id="stat-highest" class="text-3xl font-bold text-slate-800">0</p></div>
<div class="p-5 bg-white rounded-xl shadow"><h3 data-lang-key="stat_lowest" class="text-sm font-medium text-slate-500">最低分</h3><p id="stat-lowest" class="text-3xl font-bold text-slate-800">0</p></div>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<div class="p-6 bg-white rounded-xl shadow"><h3 data-lang-key="chart_distribution_title" class="text-lg font-semibold mb-4">分数段分布</h3><div class="relative h-80"><canvas id="scoreDistributionChart"></canvas></div></div>
<div class="p-6 bg-white rounded-xl shadow"><h3 data-lang-key="chart_performance_title" class="text-lg font-semibold mb-4">整体表现概览</h3><div class="relative h-80"><canvas id="performanceRateChart"></canvas></div></div>
</div>
<div class="grid gap-6 lg:grid-cols-3">
<div class="lg:col-span-2 p-6 bg-white rounded-xl shadow">
<div class="flex justify-between items-center mb-4"><h3 data-lang-key="table_title" class="text-lg font-semibold">详细分析报告</h3><button id="export-btn" data-lang-key="export_button" class="px-4 py-2 text-sm font-medium text-blue-700 bg-blue-100 rounded-lg hover:bg-blue-200">导出数据 (CSV)</button></div>
<div class="overflow-x-auto"><table class="w-full text-sm text-left text-slate-600"><thead class="text-xs text-slate-700 uppercase bg-slate-100"><tr><th data-lang-key="table_header_class" class="px-4 py-3">班级</th><th data-lang-key="table_header_students" class="px-4 py-3">人数</th><th data-lang-key="table_header_avg" class="px-4 py-3">平均分</th><th data-lang-key="table_header_stddev" class="px-4 py-3">标准差</th><th data-lang-key="table_header_exceeds" class="px-4 py-3">>85% (优秀)</th><th data-lang-key="table_header_meets_plus" class="px-4 py-3">>75% (良好)</th><th data-config-key="table_header_meets" class="px-4 py-3">>60% (及格)</th><th data-lang-key="table_header_working" class="px-4 py-3"><60% (待提高)</th><th data-lang-key="table_header_try_again" class="px-4 py-3"><35% (需努力)</th></tr></thead><tbody id="report-table-body"></tbody></table></div>
</div>
<div class="p-6 bg-white rounded-xl shadow"><h3 data-lang-key="needing_attention_title" class="text-lg font-semibold mb-4">待提高学生名单 (分数低于60)</h3><ul id="needing-attention-list" class="space-y-2 max-h-60 overflow-y-auto"></ul></div>
</div>
</div>
</div>
<!-- Grade Entry Section -->
<div id="grade-entry-section" class="page-content">
<div class="grid lg:grid-cols-2 gap-6">
<div class="space-y-6">
<div class="p-6 bg-white rounded-xl shadow">
<h3 data-lang-key="entry_manual_title" class="text-lg font-semibold">手动录入成绩</h3>
<p data-lang-key="entry_manual_subtitle" class="text-sm text-slate-500 mb-4">逐条添加学生分数。</p>
<form id="add-score-form" class="flex gap-4">
<input type="text" id="student-name" required class="flex-grow px-3 py-2 bg-slate-50 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500" data-lang-placeholder="entry_student_name" placeholder="学生姓名">
<input type="number" id="student-score" min="0" step="0.5" required class="w-28 px-3 py-2 bg-slate-50 border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500" data-lang-placeholder="entry_student_score" placeholder="分数">
<button type="submit" data-lang-key="entry_add_button" class="px-5 py-2 font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700">添加分数</button>
</form>
</div>
<div class="p-6 bg-white rounded-xl shadow">
<h3 data-lang-key="entry_import_title" class="text-lg font-semibold">从Excel批量导入</h3>
<p data-lang-key="entry_import_subtitle" class="text-sm text-slate-500 mb-4">上传包含 '姓名' 和 '分数' 列的 .xlsx 或 .csv 文件。</p>
<div id="drop-area" class="mt-1 flex justify-center px-6 pt-5 pb-6 border-2 border-slate-300 border-dashed rounded-md">
<div class="space-y-1 text-center">
<svg class="mx-auto h-12 w-12 text-slate-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true"><path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" /></svg>
<div class="flex text-sm text-slate-600">
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500">
<span data-lang-key="entry_upload_button">上传文件</span>
<input id="file-upload" name="file-upload" type="file" class="sr-only" accept=".xlsx, .xls, .csv">
</label>
<p data-lang-key="entry_drag_drop" class="pl-1">或拖拽文件</p>
</div>
<p id="file-name-display" class="text-xs text-slate-500"></p>
<p id="file-feedback" class="text-sm font-medium mt-2"></p>
</div>
</div>
</div>
</div>
<div class="p-6 bg-white rounded-xl shadow">
<div class="flex justify-between items-center mb-4">
<h3 data-lang-key="entry_table_title" class="text-lg font-semibold">当前分数列表</h3>
<div class="flex gap-3">
<button id="clear-btn" data-lang-key="entry_clear_button" class="px-4 py-2 text-sm font-medium text-red-700 bg-red-100 rounded-lg hover:bg-red-200 disabled:opacity-50 disabled:cursor-not-allowed">全部清除</button>
<button id="analyze-btn" data-lang-key="entry_analyze_button" class="px-4 py-2 text-sm font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">生成简易报告</button>
</div>
</div>
<div class="max-h-96 overflow-y-auto border rounded-lg">
<table class="w-full text-sm text-left"><thead class="text-xs text-slate-700 uppercase bg-slate-100 sticky top-0"><tr><th class="p-3 w-16" data-lang-key="entry_table_header_no">序号</th><th class="p-3" data-lang-key="entry_table_header_name">学生姓名</th><th class="p-3" data-lang-key="entry_table_header_score">分数</th><th class="p-3 text-center w-24" data-lang-key="entry_table_header_action">操作</th></tr></thead><tbody id="scores-table-body"></tbody></table>
</div>
</div>
</div>
</div>
<!-- Test Paper Analysis Section -->
<div id="test-paper-analysis-section" class="page-content hidden">
<div class="space-y-8">
<div id="analysis-upload-step" class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-900" data-lang-key="ta_upload_title">上传分析所需文件</h3>
<p class="text-sm text-slate-500" data-lang-key="ta_upload_subtitle">请上传所需文件。AI将分析数据并生成报告。</p>
<div id="analysis-error-message" class="hidden mt-4 p-4 bg-red-50 text-red-700 rounded-lg">
<h4 class="font-bold" data-lang-key="ta_error_title">分析出错</h4>
<p data-lang-key="ta_error_message">无法完成分析。请检查文件格式是否正确,或稍后重试。</p>
<pre id="analysis-error-details" class="mt-2 text-xs whitespace-pre-wrap"></pre>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mt-6">
<div>
<h4 class="text-base font-medium text-slate-800" data-lang-key="ta_upload_file1_title">*1. 学生总体成绩 (Excel格式)</h4>
<label for="overall-grades-upload" class="file-drop-area mt-2" id="overall-grades-drop-area">
<svg class="mx-auto h-10 w-10 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125V6a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625a3.375 3.375 0 0 0-3.375 3.375v11.25a3.375 3.375 0 0 0 3.375 3.375h12.75a3.375 3.375 0 0 0 3.375-3.375v-6.375" /></svg>
<p id="overall-grades-feedback" class="mt-2 text-sm text-slate-600" data-lang-key="ta_upload_placeholder">上传或拖拽文件</p>
<input id="overall-grades-upload" type="file" class="sr-only" accept=".xlsx, .xls, .csv">
</label>
</div>
<div>
<h4 class="text-base font-medium text-slate-800" data-lang-key="ta_upload_file2_title">*2. 每道题目具体得分 (Excel格式)</h4>
<label for="item-scores-upload" class="file-drop-area mt-2" id="item-scores-drop-area">
<svg class="mx-auto h-10 w-10 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125V6a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625a3.375 3.375 0 0 0-3.375 3.375v11.25a3.375 3.375 0 0 0 3.375 3.375h12.75a3.375 3.375 0 0 0 3.375-3.375v-6.375" /></svg>
<p id="item-scores-feedback" class="mt-2 text-sm text-slate-600" data-lang-key="ta_upload_placeholder">上传或拖拽文件</p>
<input id="item-scores-upload" type="file" class="sr-only" accept=".xlsx, .xls, .csv">
</label>
</div>
<div>
<h4 class="text-base font-medium text-slate-800" data-lang-key="ta_upload_file3_title">*3. 试卷原题 (PDF格式)</h4>
<p class="text-xs text-slate-500" data-lang-key="ta_upload_file3_subtitle">(必须,用于AI提取题目内容)</p>
<label for="paper-pdf-upload" class="file-drop-area mt-2" id="paper-pdf-drop-area">
<svg class="mx-auto h-10 w-10 text-slate-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125V6a3.375 3.375 0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625a3.375 3.375 0 0 0-3.375 3.375v11.25a3.375 3.375 0 0 0 3.375 3.375h12.75a3.375 3.375 0 0 0 3.375-3.375v-6.375" /></svg>
<p id="paper-pdf-feedback" class="mt-2 text-sm text-slate-600" data-lang-key="ta_upload_placeholder">上传或拖拽文件</p>
<input id="paper-pdf-upload" type="file" class="sr-only" accept=".pdf">
</label>
</div>
</div>
<!-- Low Score Boundary Input -->
<div class="mt-6 p-4 bg-slate-50 rounded-lg border border-slate-200">
<label for="low-score-boundary" class="block text-sm font-medium text-slate-700 mb-2">
<span data-lang-key="ta_low_score_boundary_label">低得分率阈值 (%)</span>
<span class="text-xs text-slate-500 ml-2" data-lang-key="ta_low_score_boundary_hint">低于此比例的题目将被分析 (默认 60%)</span>
</label>
<input type="number" id="low-score-boundary" min="0" max="100" value="60" class="w-32 px-3 py-2 bg-white border border-slate-300 rounded-md focus:ring-1 focus:ring-blue-500">
</div>
<div id="analysis-loading" class="mt-8 text-center hidden flex-col items-center justify-center">
<div class="ai-loader"></div>
<p class="mt-4 text-lg font-semibold text-blue-600" data-lang-key="ta_loading_text">AI分析中,请稍候...</p>
<p class="text-slate-500" data-lang-key="ta_loading_subtext">正在解析Excel和PDF数据并调用AI生成报告...</p>
</div>
<div class="mt-8 text-center" id="analysis-button-container">
<button id="start-analysis-btn" class="px-8 py-3 text-base font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" disabled data-lang-key="ta_start_analysis_btn">开始分析</button>
</div>
</div>
<!-- 2. ANALYSIS REPORT SECTION (Initially Hidden) -->
<div id="analysis-report-section" class="hidden space-y-8 report-section">
<!-- Report Header -->
<div class="p-6 bg-white rounded-xl shadow flex justify-between items-center">
<div>
<h2 id="ta-report-title" class="text-2xl font-bold text-slate-800" data-lang-key="ta_report_main_title">试卷分析报告</h2>
<p class="text-slate-500 mt-1" data-lang-key="ta_report_main_subtitle">基于AI的深度教学分析</p>
</div>
<div class="flex">
<button id="download-report-btn" class="px-5 py-2 font-semibold text-white bg-green-600 rounded-lg hover:bg-green-700 flex items-center gap-2" data-lang-key="ta_download_report_btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
下载报告 (HTML)
</button>
<button id="download-word-btn" class="ml-2 px-5 py-2 font-semibold text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center gap-2" data-lang-key="ta_download_word_btn">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg>
下载报告 (Word)
</button>
</div>
</div>
<!-- Upload Status -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_status_title">① 上传状态</h3>
<ul id="upload-status-list" class="mt-4 space-y-2 text-slate-700 text-base grid grid-cols-1 md:grid-cols-3 gap-4"></ul>
</div>
<!-- Basic Exam Situation -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_basic_title">② 考试基本情况</h3>
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-6 mt-4">
<div class="p-4 bg-slate-50 rounded-lg"><h4 data-lang-key="ta_basic_students" class="text-sm font-medium text-slate-500">参考人数</h4><p id="ta-stat-students" class="text-3xl font-bold text-slate-800">0</p></div>
<div class="p-4 bg-slate-50 rounded-lg"><h4 data-lang-key="ta_basic_avg" class="text-sm font-medium text-slate-500">平均分</h4><p id="ta-stat-avg" class="text-3xl font-bold text-slate-800">0.00</p></div>
<div class="p-4 bg-slate-50 rounded-lg"><h4 data-lang-key="ta_basic_highest" class="text-sm font-medium text-slate-500">最高分</h4><p id="ta-stat-highest" class="text-3xl font-bold text-slate-800">0</p></div>
<div class="p-4 bg-slate-50 rounded-lg"><h4 data-lang-key="ta_basic_lowest" class="text-sm font-medium text-slate-500">最低分</h4><p id="ta-stat-lowest" class="text-3xl font-bold text-slate-800">0</p></div>
<div class="p-4 bg-slate-50 rounded-lg"><h4 data-lang-key="ta_basic_stddev" class="text-sm font-medium text-slate-500">标准差</h4><p id="ta-stat-stddev" class="text-3xl font-bold text-slate-800">0.00</p></div>
<div class="p-4 bg-slate-50 rounded-lg"><h4 data-lang-key="ta_basic_pass_rate" class="text-sm font-medium text-slate-500">及格率</h4><p id="ta-stat-pass-rate" class="text-3xl font-bold text-slate-800">0.0%</p></div>
</div>
</div>
<!-- Score Distribution Stats -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_distribution_title">③ 成绩分布统计</h3>
<div class="overflow-x-auto mt-4">
<table class="w-full text-sm text-left text-slate-600">
<thead class="text-xs text-slate-700 uppercase bg-slate-100">
<tr>
<th class="p-3" data-lang-key="ta_dist_exceeds">优秀率 (≥85%)</th>
<th class="p-3" data-lang-key="ta_dist_meets_plus">良好率 (≥75%)</th>
<th class="p-3" data-lang-key="ta_dist_meets">及格率 (≥60%)</th>
<th class="p-3" data-lang-key="ta_dist_working">不及格率 (<60%)</th>
<th class="p-3" data-lang-key="ta_dist_try_again">极差率 (<35%)</th>
</tr>
</thead>
<tbody id="distribution-stats-table-body" class="text-base"></tbody>
</table>
</div>
</div>
<!-- Low Score Rate Question Analysis -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_low_score_title">④ 低得分率题目分析</h3>
<div id="low-score-card-container" class="mt-4 grid grid-cols-1 lg:grid-cols-2 gap-6"></div>
</div>
<!-- Knowledge Point Mastery -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_kp_mastery_title">⑤ 知识点掌握情况</h3>
<div id="kp-mastery-container" class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6"></div>
</div>
<!-- Summary Teaching Remarks -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_summary_title">⑥ 总结教学备注</h3>
<div id="summary-remarks-content" class="mt-4 text-slate-700 prose max-w-none"></div>
</div>
<!-- Student Stratification Analysis -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_stratification_title">⑦ 学生分层分析</h3>
<div id="stratification-container" class="mt-4 grid grid-cols-1 md:grid-cols-3 gap-6"></div>
</div>
<!-- Case Study: Bottom Students -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_case_study_title">⑧ 个案分析: 待提高学生</h3>
<div id="case-study-container" class="mt-4 space-y-6"></div>
</div>
<!-- Case Study Summary -->
<div class="p-6 bg-white rounded-xl shadow">
<h3 class="text-xl font-semibold text-slate-800" data-lang-key="ta_report_case_summary_title">⑨ 个案分析总结备注</h3>
<div id="case-summary-content" class="mt-4 text-slate-700 prose max-w-none"></div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmation-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden items-center justify-center">
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-sm">
<h3 id="modal-title" class="text-lg font-bold text-slate-800">确认操作</h3>
<p id="modal-text" class="mt-2 text-sm text-slate-600">您确定要继续吗?</p>
<div class="mt-6 flex justify-end gap-3">
<button id="modal-cancel-btn" class="px-4 py-2 text-sm font-medium text-slate-700 bg-slate-100 rounded-lg hover:bg-slate-200">取消</button>
<button id="modal-confirm-btn" class="px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700">确认</button>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// --- STATE MANAGEMENT ---
let studentData = [];
let currentLang = 'en';
let currentClassName = '';
let scoreDistributionChart, performanceRateChart;
// Full analysis files
let overallGradesFile = null;
let itemScoresFile = null;
let paperPdfFile = null;
// --- DOM ELEMENTS ---
const loginPage = document.getElementById('login-page');
const app = document.getElementById('app');
const loginForm = document.getElementById('login-form');
const passwordInput = document.getElementById('password');
const errorMessage = document.getElementById('error-message');
const navLinks = document.querySelectorAll('.nav-link');
const pageContents = document.querySelectorAll('.page-content');
const pageTitle = document.getElementById('page-title');
// Grade Entry (Simple)
const dashboardSection = document.getElementById('dashboard-section');
const gradeEntrySection = document.getElementById('grade-entry-section');
const welcomeMessage = document.getElementById('welcome-message');
const reportContainer = document.getElementById('report-container');
const statStudents = document.getElementById('stat-students');
const statAvg = document.getElementById('stat-avg');
const statStdDev = document.getElementById('stat-stddev');
const statHighest = document.getElementById('stat-highest');
const statLowest = document.getElementById('stat-lowest');
const needingAttentionList = document.getElementById('needing-attention-list');
const addScoreForm = document.getElementById('add-score-form');
const studentNameInput = document.getElementById('student-name');
const studentScoreInput = document.getElementById('student-score');
const scoresTableBody = document.getElementById('scores-table-body');
const fileUpload = document.getElementById('file-upload');
const fileNameDisplay = document.getElementById('file-name-display');
const fileFeedback = document.getElementById('file-feedback');
const dropArea = document.getElementById('drop-area');
const analyzeBtn = document.getElementById('analyze-btn');
const clearBtn = document.getElementById('clear-btn');
const exportBtn = document.getElementById('export-btn');
// Language & Modal
const langEnBtn = document.getElementById('lang-en');
const langZhBtn = document.getElementById('lang-zh');
const confirmationModal = document.getElementById('confirmation-modal');
const modalTitle = document.getElementById('modal-title');
const modalText = document.getElementById('modal-text');
const modalConfirmBtn = document.getElementById('modal-confirm-btn');
const modalCancelBtn = document.getElementById('modal-cancel-btn');
// Test Paper Analysis (Full)
const testPaperAnalysisSection = document.getElementById('test-paper-analysis-section');
const analysisUploadStep = document.getElementById('analysis-upload-step');
const analysisReportSection = document.getElementById('analysis-report-section');
const analysisLoading = document.getElementById('analysis-loading');
const analysisButtonContainer = document.getElementById('analysis-button-container');
const analysisErrorMessage = document.getElementById('analysis-error-message');
const analysisErrorDetails = document.getElementById('analysis-error-details');
const startAnalysisBtn = document.getElementById('start-analysis-btn');
const lowScoreBoundaryInput = document.getElementById('low-score-boundary');
// File Drop Areas & Uploads (Standard ID mapping)
const overallGradesDropArea = document.getElementById('overall-grades-drop-area');
const itemScoresDropArea = document.getElementById('item-scores-drop-area');
const paperPdfDropArea = document.getElementById('paper-pdf-drop-area');
const overallGradesUpload = document.getElementById('overall-grades-upload');
const itemScoresUpload = document.getElementById('item-scores-upload');
const paperPdfUpload = document.getElementById('paper-pdf-upload');
const overallGradesFeedback = document.getElementById('overall-grades-feedback');
const itemScoresFeedback = document.getElementById('item-scores-feedback');
const paperPdfFeedback = document.getElementById('paper-pdf-feedback');
// Report Sections
const taReportTitle = document.getElementById('ta-report-title');
const uploadStatusList = document.getElementById('upload-status-list');
const taStatStudents = document.getElementById('ta-stat-students');
const taStatAvg = document.getElementById('ta-stat-avg');
const taStatHighest = document.getElementById('ta-stat-highest');
const taStatLowest = document.getElementById('ta-stat-lowest');
const taStatStddev = document.getElementById('ta-stat-stddev');
const taStatPassRate = document.getElementById('ta-stat-pass-rate');
const distributionStatsTableBody = document.getElementById('distribution-stats-table-body');
const lowScoreCardContainer = document.getElementById('low-score-card-container');
const kpMasteryContainer = document.getElementById('kp-mastery-container');
const summaryRemarksContent = document.getElementById('summary-remarks-content');
const stratificationContainer = document.getElementById('stratification-container');
const caseStudyContainer = document.getElementById('case-study-container');
const caseSummaryContent = document.getElementById('case-summary-content');
const downloadReportBtn = document.getElementById('download-report-btn');
const downloadWordBtn = document.getElementById('download-word-btn');
const translations = {
en: {
title: "Online Grade Analysis Platform", login_title: "Online Grade Analysis Platform", login_subtitle: "Please enter access password", login_error: "Invalid password.", login_button: "Enter System", app_title: "Grade Analysis",
nav_grade_entry: "Grade Entry (Simple)", nav_test_paper_analysis: "Test Analysis (Full)",
page_title_grade_entry: "Grade Entry (Simple)", page_title_test_paper_analysis: "Test Paper Analysis (Full)",
welcome_title: "Welcome to the Grade Analysis Platform", welcome_text: "To get started, please input or upload student scores in the \"Grade Entry\" section below for a simple report.", welcome_text_2: "For multi-file comprehensive analysis, please use the \"Test Analysis\" feature.",
stat_students: "Number of Students", stat_avg: "Average Score", stat_stddev: "Standard Deviation", stat_highest: "Highest Score", stat_lowest: "Lowest Score", chart_distribution_title: "Score Distribution", chart_performance_title: "Performance Overview", table_title: "Detailed Analysis Report", export_button: "Export Data (CSV)", table_header_class: "Class", table_header_students: "Students", table_header_avg: "Avg Score", table_header_stddev: "Std Dev", table_header_exceeds: ">85% (Exceeds)", table_header_meets_plus: ">75% (Meets +)", table_header_meets: ">60% (Meets)", table_header_working: "<60% (Working Toward)", table_header_try_again: "<35% (Try Again)", entry_manual_title: "Manual Grade Entry", entry_manual_subtitle: "Add student scores one by one.", entry_student_name: "Student Name", entry_student_score: "Score", entry_add_button: "Add Score", entry_import_title: "Batch Import from Excel", entry_import_subtitle: "Upload an .xlsx or .csv file with 'Name' and 'Score' columns.", entry_upload_button: "Upload a file", entry_drag_drop: "or drag and drop", entry_table_title: "Current Scores", entry_analyze_button: "Generate Simple Report", entry_clear_button: "Clear All", entry_table_header_no: "#", entry_table_header_name: "Student Name", entry_table_header_score: "Score", entry_table_header_action: "Action", entry_table_no_data: "No data yet. Add scores above or import a file.",
needing_attention_title: "Students Needing Attention (Score < 60)",
all_students_passed: "Excellent work! All students have passed.",
chart_label_exceeds: 'Exceeds (>85%)', chart_label_meets_plus: 'Meets+ (75-85%)', chart_label_meets: 'Meets (60-75%)', chart_label_working_toward: 'Working Toward (<60%)', confirm_clear_title: 'Confirm Deletion', confirm_clear_text: 'Are you sure you want to clear all student data? This action cannot be undone.', file_error_missing_cols: 'Import failed. File must contain "Name" and "Score" columns.', file_error_missing_name: 'Import failed. Could not find a "Name" column.', file_error_missing_score: 'Import failed. Could not find a "Score" column.', file_success_replace: (count) => `Previous data cleared. Successfully imported ${count} new records.`, manual_entry_class_name: "Manual Entry Class",
ta_upload_title: "Upload Files for Analysis", ta_upload_subtitle: "Please upload the required files. The AI will analyze the data and generate a report.", ta_upload_file1_title: "*1. Overall Student Grades (Excel)", ta_upload_file2_title: "*2. Itemized Student Scores (Excel)", ta_upload_file3_title: "*3. Original Test Paper (PDF)", ta_upload_file3_subtitle: "(Required, for AI to extract question text)", ta_upload_placeholder: "Upload or drag file", ta_start_analysis_btn: "Start Analysis", ta_loading_text: "AI Analysis in progress, please wait...", ta_loading_subtext: "Parsing Excel & PDF data and calling AI...", ta_report_main_title: "Test Paper Analysis Report", ta_report_main_subtitle: "In-depth Teaching Analysis Powered by AI", ta_download_report_btn: "Download Report (HTML)", ta_download_word_btn: "Download Report (Word)", ta_report_status_title: "① Upload Status", ta_report_basic_title: "② Basic Exam Situation", ta_basic_students: "Participants", ta_basic_avg: "Average", ta_basic_highest: "Highest", ta_basic_lowest: "Lowest", ta_basic_stddev: "Std. Dev.", ta_basic_pass_rate: "Pass Rate (≥60%)", ta_report_distribution_title: "③ Score Distribution Statistics", ta_dist_exceeds: "Exceeds (≥85%)", ta_dist_meets_plus: "Meets+ (≥75%)", ta_dist_meets: "Meets (≥60%)", ta_dist_working: "Working Toward (<60%)", ta_dist_try_again: "Try Again (<35%)", ta_report_low_score_title: "④ Low Scoring Rate Question Analysis", ta_low_q_header_no: "Q#", ta_low_q_header_rate: "Score Rate", ta_low_q_header_kp: "Knowledge Point & Question Text", ta_low_q_header_error: "Main Error Type", ta_low_q_header_solution: "Teaching Strategy Solution", ta_report_kp_mastery_title: "⑤ Knowledge Point Mastery", ta_report_summary_title: "⑥ Summary Teaching Remarks", ta_report_stratification_title: "⑦ Student Stratification Analysis", ta_report_case_study_title: "⑧ Case Study: Students Working Toward", ta_report_case_summary_title: "⑨ Case Study Summary Remarks", file_upload_success: (name) => `File uploaded: ${name}`, file_upload_ready: "Ready", ta_error_title: "Analysis Error", ta_error_message: "Could not complete analysis. Please check file formats or try again.", ta_error_details_prefix: "Details:", ta_error_check_format: "Error: Please check the 'Overall Grades' file format. Could not find student names or scores.", ta_error_check_format_item: "Error: Please check the 'Itemized Scores' file. Could not find the '得分率' (Score Rate) row at the end of the file.", ta_error_api_failed: "Error: AI analysis failed. The API may be unavailable.", ta_error_pdf_parse: "Error: Could not parse PDF file.", ta_case_study_issues: "Main Issues", ta_case_study_strategies: "Improvement Strategies", ta_low_score_boundary_label: "Low Score Threshold (%)", ta_low_score_boundary_hint: "Questions below this rate will be analyzed (Default 60%)"
},
zh: {
title: "在线成绩分析平台", login_title: "在线成绩分析平台", login_subtitle: "请输入访问密码", login_error: "密码无效,请重试。", login_button: "进入系统", app_title: "成绩分析",
nav_grade_entry: "成绩录入 (简易)", nav_test_paper_analysis: "试卷分析 (综合)",
page_title_grade_entry: "成绩录入 (简易)", page_title_test_paper_analysis: "试卷分析 (综合)",
welcome_title: "欢迎使用成绩分析平台", welcome_text: "请在下方“成绩录入”板块输入或上传学生分数以生成简易报告。", welcome_text_2: "如需多文件综合分析,请使用“试卷分析”功能。",
stat_students: "学生人数", stat_avg: "平均分", stat_stddev: "标准差", stat_highest: "最高分", stat_lowest: "最低分", chart_distribution_title: "分数段分布", chart_performance_title: "整体表现概览", table_title: "详细分析报告", export_button: "导出数据 (CSV)", table_header_class: "班级", table_header_students: "人数", table_header_avg: "平均分", table_header_stddev: "标准差", table_header_exceeds: ">85% (优秀)", table_header_meets_plus: ">75% (良好)", table_header_meets: ">60% (及格)", table_header_working: "<60% (待提高)", table_header_try_again: "<35% (需努力)", entry_manual_title: "手动录入成绩", entry_manual_subtitle: "逐条添加学生分数。", entry_student_name: "学生姓名", entry_student_score: "分数", entry_add_button: "添加分数", entry_import_title: "从Excel批量导入", entry_import_subtitle: "上传包含 '姓名' 和 '分数' 列的 .xlsx 或 .csv 文件。", entry_upload_button: "上传文件", entry_drag_drop: "或拖拽文件", entry_table_title: "当前分数列表", entry_analyze_button: "生成简易报告", entry_clear_button: "全部清除", entry_table_header_no: "序号", entry_table_header_name: "学生姓名", entry_table_header_score: "分数", entry_table_header_action: "操作", entry_table_no_data: "暂无数据。请在上方添加分数或导入文件。",
needing_attention_title: "待提高学生名单 (分数低于60)",
all_students_passed: "表现优异,所有学生均已及格!",
chart_label_exceeds: '优秀 (>85%)', chart_label_meets_plus: '良好 (75-85%)', chart_label_meets: '及格 (60-75%)', chart_label_working_toward: '待提高 (<60%)', confirm_clear_title: '确认清除', confirm_clear_text: '您确定要清除所有学生数据吗?此操作无法撤销。', file_error_missing_cols: '导入失败。文件必须包含“姓名”和“分数”列。', file_error_missing_name: '导入失败。文件中未找到“姓名”或类似列。', file_error_missing_score: '导入失败。文件中未找到“分数”或类似列。', file_success_replace: (count) => `已清除旧数据。成功导入 ${count} 条新记录。`, manual_entry_class_name: "手动录入班级",
ta_upload_title: "上传分析所需文件", ta_upload_subtitle: "请上传所需文件。AI将分析数据并生成报告。", ta_upload_file1_title: "*1. 学生总体成绩 (Excel格式)", ta_upload_file2_title: "*2. 每道题目具体得分 (Excel格式)", ta_upload_file3_title: "*3. 试卷原题 (PDF格式)", ta_upload_file3_subtitle: "(必须,用于AI提取题目内容)", ta_upload_placeholder: "上传或拖拽文件", ta_start_analysis_btn: "开始分析", ta_loading_text: "AI分析中,请稍候...", ta_loading_subtext: "正在解析Excel和PDF数据并调用AI生成报告...", ta_report_main_title: "试卷分析报告", ta_report_main_subtitle: "基于AI的深度教学分析", ta_download_report_btn: "下载报告 (HTML)", ta_download_word_btn: "下载报告 (Word)", ta_report_status_title: "① 上传状态", ta_report_basic_title: "② 考试基本情况", ta_basic_students: "参考人数", ta_basic_avg: "平均分", ta_basic_highest: "最高分", ta_basic_lowest: "最低分", ta_basic_stddev: "标准差", ta_basic_pass_rate: "及格率 (≥60%)", ta_report_distribution_title: "③ 成绩分布统计", ta_dist_exceeds: "优秀率 (≥85%)", ta_dist_meets_plus: "良好率 (≥75%)", ta_dist_meets: "及格率 (≥60%)", ta_dist_working: "不及格率 (<60%)", ta_dist_try_again: "极差率 (<35%)", ta_report_low_score_title: "④ 低得分率题目分析", ta_low_q_header_no: "题号", ta_low_q_header_rate: "得分率", ta_low_q_header_kp: "知识点 & 题目原文", ta_low_q_header_error: "主要错误类型", ta_low_q_header_solution: "教学策略解决方案", ta_report_kp_mastery_title: "⑤ 知识点掌握情况", ta_report_summary_title: "⑥ 总结教学备注", ta_report_stratification_title: "⑦ 学生分层分析", ta_report_case_study_title: "⑧ 个案分析: 待提高学生", ta_report_case_summary_title: "⑨ 个案分析总结备注", file_upload_success: (name) => `文件已上传: ${name}`, file_upload_ready: "准备就绪", ta_error_title: "分析出错", ta_error_message: "无法完成分析。请检查文件格式是否正确,或稍后重试。", ta_error_details_prefix: "错误详情:", ta_error_check_format: "错误:请检查'学生总体成绩'文件格式。未能在文件中找到学生姓名或分数。", ta_error_check_format_item: "错误:请检查'每道题目具体得分'文件。未能在文件末尾找到'得分率'行。", ta_error_api_failed: "错误:AI分析失败。API可能无法访问或返回错误。", ta_error_pdf_parse: "错误:无法解析PDF文件。", ta_case_study_issues: "主要问题", ta_case_study_strategies: "改进策略", ta_low_score_boundary_label: "低得分率阈值 (%)", ta_low_score_boundary_hint: "低于此比例的题目将被分析 (默认 60%)"
}
};
// --- FUNCTIONS ---
function setLanguage(lang) {
currentLang = lang;
document.documentElement.lang = lang;
document.querySelectorAll('[data-lang-key]').forEach(el => {
const key = el.getAttribute('data-lang-key');
const translation = translations[lang][key];
if (translation && typeof translation !== 'function') {
el.textContent = translation;
}
});
document.querySelectorAll('[data-lang-placeholder]').forEach(el => {
const key = el.getAttribute('data-lang-placeholder');
if (translations[lang][key]) {
el.placeholder = translations[lang][key];
}
});
if (lang === 'en') {
langEnBtn.classList.add('bg-white', 'text-blue-600', 'shadow-sm');
langEnBtn.classList.remove('text-slate-600');
langZhBtn.classList.remove('bg-white', 'text-blue-600', 'shadow-sm');
langZhBtn.classList.add('text-slate-600');
} else {
langZhBtn.classList.add('bg-white', 'text-blue-600', 'shadow-sm');
langZhBtn.classList.remove('text-slate-600');
langEnBtn.classList.remove('bg-white', 'text-blue-600', 'shadow-sm');
langEnBtn.classList.add('text-slate-600');
}
if (currentClassName === translations.en.manual_entry_class_name || currentClassName === translations.zh.manual_entry_class_name) {
currentClassName = translations[lang].manual_entry_class_name;
}
if (reportContainer.style.display !== 'none' && studentData.length > 0) {
generateReport();
}
// Update file feedback text on lang change
updateFileFeedback(overallGradesFile, overallGradesFeedback);
updateFileFeedback(itemScoresFile, itemScoresFeedback);
updateFileFeedback(paperPdfFile, paperPdfFeedback);
}
function handleLogin(e) {
e.preventDefault();
const inputPwd = passwordInput.value.trim();
if (inputPwd) {
// Store password locally to send with API request later
localStorage.setItem('app_password', inputPwd);
loginPage.classList.add('hidden');
app.classList.remove('hidden');
document.body.classList.remove('locked');
} else {
// simple visual feedback
passwordInput.classList.add('border-red-500');
}
}
// ... (Navigation, Table Update, File Handling functions remain the same) ...
function handleNavigation(e) {
e.preventDefault();
const targetId = e.currentTarget.getAttribute('href').substring(1);
navLinks.forEach(link => link.classList.remove('active'));
e.currentTarget.classList.add('active');
pageContents.forEach(content => content.classList.add('hidden'));
if (targetId === 'grade-entry') {
dashboardSection.classList.remove('hidden');
gradeEntrySection.classList.remove('hidden');
} else {
document.getElementById(`${targetId}-section`).classList.remove('hidden');
}
const titleKey = `page_title_${targetId.replace('-', '_')}`;
pageTitle.textContent = translations[currentLang][titleKey] || 'Page';
pageTitle.setAttribute('data-lang-key', titleKey);
}
function updateTable() {
scoresTableBody.innerHTML = '';
if (studentData.length === 0) {
scoresTableBody.innerHTML = `<tr><td colspan="4" class="text-center p-4 text-slate-500" data-lang-key="entry_table_no_data">${translations[currentLang].entry_table_no_data}</td></tr>`;
analyzeBtn.disabled = true;
clearBtn.disabled = true;
return;
}
studentData.forEach((student, index) => {
const tr = document.createElement('tr');
tr.className = 'border-b';
tr.innerHTML = `<td class="p-3">${index + 1}</td><td class="p-3">${student.name}</td><td class="p-3">${student.score}</td><td class="p-3 text-center"><button class="text-red-500 hover:text-red-700 remove-btn" data-index="${index}"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg></button></td>`;
scoresTableBody.appendChild(tr);
});
analyzeBtn.disabled = false;
clearBtn.disabled = false;
}
function addScore(name, score) {
if (studentData.length === 0) { currentClassName = translations[currentLang].manual_entry_class_name; }
studentData.push({ name, score: parseFloat(score) });
updateTable();
}
function removeScore(index) { studentData.splice(index, 1); updateTable(); }
function handleFile(file) {
if (!file) return;
fileNameDisplay.textContent = file.name;
fileFeedback.textContent = '';
fileFeedback.classList.remove('text-red-500', 'text-green-600');
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result);
const workbook = XLSX.read(data, {type: 'array'});
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" });
if (rows.length === 0) { fileFeedback.textContent = '文件为空或格式不正确。'; fileFeedback.classList.add('text-red-500'); return; }
let headerRow = rows[0].map(h => String(h).toLowerCase().trim());
let dataRows = rows.slice(1);
const nameSynonyms = ['name', 'student name', 'student', '姓名'];
const scoreSynonyms = ['score', 'grade', '分数', '成绩'];
let nameIndex = headerRow.findIndex(h => nameSynonyms.includes(h));
let scoreIndex = headerRow.findIndex(h => scoreSynonyms.includes(h));
if (nameIndex === -1 || scoreIndex === -1) { dataRows = rows; nameIndex = 0; scoreIndex = 1; }
const importedData = dataRows.map(row => ({ name: row[nameIndex], score: parseFloat(row[scoreIndex]) })).filter(d => d.name && !isNaN(d.score) && d.score >= 0);
if(importedData.length > 0) { studentData = importedData; currentClassName = file.name.replace(/\.(xlsx|xls|csv)$/i, ''); updateTable(); fileFeedback.textContent = translations[currentLang].file_success_replace(importedData.length); fileFeedback.classList.add('text-green-600'); } else { fileFeedback.textContent = '在文件中未找到有效的数据。'; fileFeedback.classList.add('text-red-500'); }
} catch (error) { console.error("File parsing error:", error); fileFeedback.textContent = "读取文件时发生错误。"; fileFeedback.classList.add('text-red-500'); } finally { fileUpload.value = null; }
};
reader.readAsArrayBuffer(file);
}
function calculateStats() {
const scores = studentData.map(s => s.score);
const count = scores.length;
if (count === 0) return { count: 0, avg: 0, stdDev: 0, highest: 0, lowest: 0, distribution: [0,0,0,0,0], performance: {exceeds: 0, meets_plus: 0, meets: 0, working_toward: 0} };
const sum = scores.reduce((a, b) => a + b, 0);
const avg = sum / count;
const stdDev = Math.sqrt(scores.map(x => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / count);
const highest = Math.max(...scores);
const lowest = Math.min(...scores);
const distribution = [0, 0, 0, 0, 0];
const performance = {exceeds: 0, meets_plus: 0, meets: 0, working_toward: 0, try_again: 0};
scores.forEach(score => {
if (score < 60) distribution[0]++; else if (score < 70) distribution[1]++; else if (score < 80) distribution[2]++; else if (score < 90) distribution[3]++; else distribution[4]++;
if(score >= 85) performance.exceeds++; if(score >= 75) performance.meets_plus++; if(score >= 60) performance.meets++; if(score < 60) performance.working_toward++; if(score < 35) performance.try_again++;
});
return { count, avg: avg.toFixed(2), stdDev: stdDev.toFixed(2), highest, lowest, distribution, performance };
}
function generateReport() {
if (studentData.length === 0) return;
const stats = calculateStats();
welcomeMessage.classList.add('hidden');
reportContainer.classList.remove('hidden');
statStudents.textContent = stats.count; statAvg.textContent = stats.avg; statStdDev.textContent = stats.stdDev; statHighest.textContent = stats.highest; statLowest.textContent = stats.lowest;
const chartCtx1 = document.getElementById('scoreDistributionChart').getContext('2d');
if (scoreDistributionChart) scoreDistributionChart.destroy();
scoreDistributionChart = new Chart(chartCtx1, { type: 'bar', data: { labels: ['0-59', '60-69', '70-79', '80-89', '90-100'], datasets: [{ data: stats.distribution, backgroundColor: 'rgba(59, 130, 246, 0.5)', borderColor: 'rgba(59, 130, 246, 1)', borderWidth: 1, borderRadius: 4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } } });
const chartCtx2 = document.getElementById('performanceRateChart').getContext('2d');
if (performanceRateChart) performanceRateChart.destroy();
performanceRateChart = new Chart(chartCtx2, { type: 'doughnut', data: { labels: [translations[currentLang].chart_label_exceeds, translations[currentLang].chart_label_meets_plus, translations[currentLang].chart_label_meets, translations[currentLang].chart_label_working_toward], datasets: [{ data: [stats.performance.exceeds, stats.performance.meets_plus - stats.performance.exceeds, stats.performance.meets - stats.performance.meets_plus, stats.performance.working_toward], backgroundColor: ['#10B981', '#3B82F6', '#F59E0B', '#EF4444'], }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' } } } });
document.getElementById('report-table-body').innerHTML = `<tr class="bg-white border-b"><td class="p-4 font-medium">${currentClassName}</td><td class="p-4">${stats.count}</td><td class="p-4">${stats.avg}</td><td class="p-4">${stats.stdDev}</td><td class="p-4">${stats.performance.exceeds} (${(stats.performance.exceeds/stats.count*100).toFixed(1)}%)</td><td class="p-4">${stats.performance.meets_plus} (${(stats.performance.meets_plus/stats.count*100).toFixed(1)}%)</td><td class="p-4">${stats.performance.meets} (${(stats.performance.meets/stats.count*100).toFixed(1)}%)</td><td class="p-4">${stats.performance.working_toward} (${(stats.performance.working_toward/stats.count*100).toFixed(1)}%)</td><td class="p-4">${stats.performance.try_again} (${(stats.performance.try_again/stats.count*100).toFixed(1)}%)</td></tr>`;
const needingAttentionStudents = studentData.filter(s => s.score < 60).sort((a,b) => b.score - a.score);
needingAttentionList.innerHTML = '';
if(needingAttentionStudents.length > 0) { needingAttentionStudents.forEach(student => { const li = document.createElement('li'); li.className = "flex justify-between items-center py-2 px-3 border-b border-slate-100 last:border-b-0"; li.innerHTML = `<span class="text-slate-700">${student.name}</span><span class="font-bold text-red-600">${student.score}</span>`; needingAttentionList.appendChild(li); }); } else { needingAttentionList.innerHTML = `<p class="text-center text-slate-500 py-4" data-lang-key="all_students_passed">${translations[currentLang].all_students_passed}</p>`; }
}
function exportToCsv() {
const stats = calculateStats();
let csvContent = "data:text/csv;charset=utf-8,\uFEFF"; // Add BOM for Excel UTF-8 support
const headers = [
translations[currentLang].table_header_class,
translations[currentLang].table_header_students,
translations[currentLang].table_header_avg,
translations[currentLang].table_header_stddev,
translations[currentLang].table_header_exceeds,
translations[currentLang].table_header_meets_plus,
translations[currentLang].table_header_meets,
translations[currentLang].table_header_working,
translations[currentLang].table_header_try_again,
];
// Fix: ensure proper string concatenation and quoting
const dataRow = [
currentClassName,
stats.count,
stats.avg,
stats.stdDev,
`${stats.performance.exceeds} (${(stats.performance.exceeds/stats.count*100).toFixed(1)}%)`,
`${stats.performance.meets_plus} (${(stats.performance.meets_plus/stats.count*100).toFixed(1)}%)`,
`${stats.performance.meets} (${(stats.performance.meets/stats.count*100).toFixed(1)}%)`,
`${stats.performance.working_toward} (${(stats.performance.working_toward/stats.count*100).toFixed(1)}%)`,
`${stats.performance.try_again} (${(stats.performance.try_again/stats.count*100).toFixed(1)}%)`,
];
csvContent += headers.map(h => `"${h}"`).join(",") + "\n";
csvContent += dataRow.map(d => `"${d}"`).join(",") + "\n";
const needingAttentionStudents = studentData
.filter(s => s.score < 60)
.sort((a, b) => b.score - a.score);
if (needingAttentionStudents.length > 0) {
csvContent += "\n\n";
csvContent += `"${translations[currentLang].needing_attention_title}"\n`;
csvContent += `"${translations[currentLang].entry_table_header_name}","${translations[currentLang].entry_table_header_score}"\n`;
needingAttentionStudents.forEach(student => {
csvContent += `"${student.name}",${student.score}\n`;
});
}
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", `${currentClassName || 'report'}_analysis_report.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
function showConfirmationModal(titleKey, textKey, onConfirm) { modalTitle.textContent = translations[currentLang][titleKey]; modalText.textContent = translations[currentLang][textKey]; confirmationModal.classList.remove('hidden'); confirmationModal.classList.add('flex'); const newConfirmBtn = modalConfirmBtn.cloneNode(true); modalConfirmBtn.parentNode.replaceChild(newConfirmBtn, modalConfirmBtn); newConfirmBtn.addEventListener('click', () => { onConfirm(); hideConfirmationModal(); }, { once: true }); }
function hideConfirmationModal() { confirmationModal.classList.add('hidden'); confirmationModal.classList.remove('flex'); }
// --- Test Paper Analysis Functions ---
function setupFileUploader(dropArea, uploadInput, feedbackEl, fileStateSetter) {
const dragEvents = ['dragenter', 'dragover', 'dragleave', 'drop'];
dragEvents.forEach(eventName => { dropArea.addEventListener(eventName, (e) => { e.preventDefault(); e.stopPropagation(); if (eventName === 'dragenter' || eventName === 'dragover') { dropArea.classList.add('dragover'); } else { dropArea.classList.remove('dragover'); } if (eventName === 'drop') { const file = e.dataTransfer.files[0]; uploadInput.files = e.dataTransfer.files; handleFileChange(file, feedbackEl, fileStateSetter); } }, false); });
uploadInput.addEventListener('change', (e) => { const file = e.target.files[0]; handleFileChange(file, feedbackEl, fileStateSetter); });
}
function handleFileChange(file, feedbackEl, fileStateSetter) { if (file) { fileStateSetter(file); updateFileFeedback(file, feedbackEl); } checkAllFilesUploaded(); }
function updateFileFeedback(file, feedbackEl) { if (file) { feedbackEl.textContent = translations[currentLang].file_upload_success(file.name); feedbackEl.classList.add('text-green-600', 'font-semibold'); feedbackEl.classList.remove('text-slate-600'); } else { feedbackEl.textContent = translations[currentLang].ta_upload_placeholder; feedbackEl.classList.remove('text-green-600', 'font-semibold'); feedbackEl.classList.add('text-slate-600'); } }
function checkAllFilesUploaded() { if (overallGradesFile && itemScoresFile && paperPdfFile) { startAnalysisBtn.disabled = false; } else { startAnalysisBtn.disabled = true; } }
function parseExcel(file) { return new Promise((resolve, reject) => { if (!file) { return reject(new Error("No file provided")); } const reader = new FileReader(); reader.onload = (e) => { try { const data = new Uint8Array(e.target.result); const workbook = XLSX.read(data, { type: 'array' }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const json = XLSX.utils.sheet_to_json(worksheet, { header: 1 }); resolve({data: json, name: file.name}); } catch (error) { reject(error); } }; reader.onerror = (error) => reject(error); reader.readAsArrayBuffer(file); }); }
// --- UPDATED PARSE PDF FOR TEXT ---
async function parsePdfForText(file) {
if (!file) return null;
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
let allText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
// Add X/Y coordinates to items to allow spatial sorting
const items = textContent.items.map(item => {
const tx = item.transform; // [scaleX, skewY, skewX, scaleY, translateX, translateY]
return {
str: item.str,
x: tx[4],
y: tx[5], // Higher Y is higher on page (bottom-left origin)
hasEOL: item.hasEOL,
height: item.height
};
});
// Sort items: Top-to-Bottom (Y Desc), then Left-to-Right (X Asc)
// We allow a small 'row tolerance' so slightly misaligned items are treated as same line
items.sort((a, b) => {
const yDiff = b.y - a.y;
if (Math.abs(yDiff) > 8) { // Tolerance of 8 units for a "line"
return yDiff;
}
return a.x - b.x;
});
allText += `\n--- Page ${i} ---\n`;
// Reconstruct text with newlines
let lastY = items[0]?.y || 0;
items.forEach(item => {
// If Y drops significantly, insert a newline
if (lastY - item.y > 10) {
allText += '\n';
}
allText += item.str + ' '; // Add space between words
lastY = item.y;
});
allText += '\n';
}
return allText;
} catch (error) {
console.error("PDF parsing error:", error);
throw new Error(translations[currentLang].ta_error_pdf_parse);
}
}
function processStudentData(overallGrades, itemScores) { /* ... existing logic ... */ let overallData = []; let nameCol = '姓名'; let scoreCol = '分数'; let headerRow = overallGrades.data[0].map(h => String(h).toLowerCase().trim()); let dataRows; let nameIndex = headerRow.findIndex(h => h.includes('name') || h.includes('姓名')); let scoreIndex = headerRow.findIndex(h => h.includes('score') || h.includes('分数') || h.includes('成绩') || h.includes('总分')); if (nameIndex !== -1 && scoreIndex !== -1) { dataRows = overallGrades.data.slice(1); nameCol = overallGrades.data[0][nameIndex]; scoreCol = overallGrades.data[0][scoreIndex]; } else { nameIndex = 0; scoreIndex = 1; dataRows = overallGrades.data; if (dataRows.length > 0 && isNaN(parseFloat(dataRows[0][scoreIndex]))) { dataRows = dataRows.slice(1); } } overallData = dataRows.map(row => ({ name: row[nameIndex], score: parseFloat(row[scoreIndex]) })).filter(d => d.name && !isNaN(d.score)); if (overallData.length === 0) { throw new Error(translations[currentLang].ta_error_check_format); } const scores = overallData.map(s => s.score); const count = scores.length; const sum = scores.reduce((a, b) => a + b, 0); const avg = sum / count; const stdDev = Math.sqrt(scores.map(x => Math.pow(x - avg, 2)).reduce((a, b) => a + b, 0) / count); const highest = Math.max(...scores); const lowest = Math.min(...scores); let exceeds = 0, meets_plus = 0, meets = 0, working_toward = 0, try_again = 0; scores.forEach(score => { if(score >= 85) exceeds++; if(score >= 75) meets_plus++; if(score >= 60) meets++; if(score < 60) working_toward++; if(score < 35) try_again++; }); const stats = { count, avg: avg.toFixed(1), highest, lowest, stdDev: stdDev.toFixed(1), passRate: (meets / count * 100).toFixed(1) + '%' }; const distribution = { exceeds: (exceeds / count * 100).toFixed(1) + '%', meets_plus: (meets_plus / count * 100).toFixed(1) + '%', meets: (meets / count * 100).toFixed(1) + '%', working_toward: (working_toward / count * 100).toFixed(1) + '%', try_again: (try_again / count * 100).toFixed(1) + '%' }; let itemHeaderRow = itemScores.data[0].map(h => String(h).trim()); let itemNameCol = itemHeaderRow[0]; let questionCols = itemHeaderRow.slice(1); let scoreRateRowIndex = -1; for(let i = itemScores.data.length - 1; i >= 0; i--) { if (String(itemScores.data[i][0]).trim().includes('得分率')) { scoreRateRowIndex = i; break; } } if (scoreRateRowIndex === -1) { throw new Error(translations[currentLang].ta_error_check_format_item); } let scoreRateRow = itemScores.data[scoreRateRowIndex]; let scoreRates = scoreRateRow.slice(1); let studentItemDataRows = itemScores.data.slice(1, scoreRateRowIndex); let itemAnalysis = []; for(let i = 0; i < questionCols.length; i++) { if (i >= scoreRates.length) break; const qName = questionCols[i]; const qScoreRate = String(scoreRates[i]); let qTotalScore = 0; let validStudents = 0; studentItemDataRows.forEach(row => { let score = parseFloat(row[i+1]); if (isNaN(score)) { score = 0; } qTotalScore += score; validStudents++; }); const qAvg = validStudents > 0 ? qTotalScore / validStudents : 0; itemAnalysis.push({ question: qName, scoreRate: qScoreRate, avgScore: qAvg.toFixed(2) }); } const bottomStudents = overallData.filter(s => s.score < 60).sort((a,b) => a.score - b.score).slice(0, 5); return { stats, distribution, itemAnalysis, bottomStudents, className: overallGrades.name.replace(/\.(xlsx|xls|csv)$/i, '') }; }
// --- UPDATED AI PROMPT ---
function createAIPrompt(analysisData, pdfText, threshold) {
const lang = currentLang === 'zh' ? 'Chinese' : 'English';
const fullPdfText = pdfText || "Not provided";
const userThreshold = threshold || 60;
const prompt = `
You are an expert teaching analyst. I provide you with test statistics and the original PDF text content.
Your task is to analyze this data and provide pedagogical feedback in ${lang}.
**Item Analysis (Questions & Score Rates):**
${JSON.stringify(analysisData.itemAnalysis, null, 2)}
**Original Paper Text (Extracted from PDF):**
${fullPdfText}
**Bottom 5 Performing Students (Name, Score):**
${JSON.stringify(analysisData.bottomStudents, null, 2)}
**CRITICAL INSTRUCTION FOR "lowScoreAnalysis":**
1. FILTER: Only analyze questions where 'scoreRate' is LESS THAN ${userThreshold}%.
2. LOCATE TEXT: For each filtered question (e.g., "Q17"), you MUST find the exact text in the provided PDF content.
- Look for the number at the START of a line (e.g., "\n17", "\n17)", "\nQ17").
- DO NOT confuse Question 17 with the number "17" appearing inside the text of Question 16.
- **ANTI-DRIFT CHECK:** The text for Question X MUST appear after Question X-1 and before Question X+1.
- If you see "16 ... [text] ... 18", and no "17" starting a line in between, then Q17 text is missing. State "Text not found".
- **Do NOT hallucinate.** If you cannot find the exact text starting with "17", do not invent it.
- **STRICT INSTRUCTION FOR "caseStudies":**
1. You must ONLY generate case studies for the specific students listed in the "**Bottom 5 Performing Students**" JSON data provided above.
2. **COPY the "name" and "score" EXACTLY** from that list. Do not invent names or change scores.
3. If the list is empty (no students < 60), return an empty array [].
4. For "problems" and "strategies", infer likely weaknesses based on the *general* low-scoring questions found in the paper, tailored to how low their specific score is.
Return a JSON object matching this schema:
{
"lowScoreAnalysis": [
{
"question": "Q17",
"scoreRate": "45%",
"questionText": "exact text found in PDF...",
"knowledgePoint": "Topic...",
"errorType": "Reason...",
"solution": ["Strategy 1", "Strategy 2"]
}
],
"kpMastery": [ { "name": "Topic", "rate": 0.85, "errors": ["..."] } ],
"summaryRemarks": ["..."],
"stratification": { "high": [], "medium": [], "low": [] },
"caseStudies": [],
"caseSummary": []
}
`;
return prompt;
}
// ... (AI Schema, Call, Retry functions remain the same) ...
const aiResponseSchema = { "type": "OBJECT", "required": ["lowScoreAnalysis", "kpMastery", "summaryRemarks", "stratification", "caseStudies", "caseSummary"], "properties": { "lowScoreAnalysis": { "type": "ARRAY", "items": { "type": "OBJECT", "required": ["question", "scoreRate", "questionText", "knowledgePoint", "errorType", "solution"], "properties": { "question": { "type": "STRING" }, "scoreRate": { "type": "STRING" }, "questionText": { "type": "STRING" }, "knowledgePoint": { "type": "STRING" }, "errorType": { "type": "STRING" }, "solution": { "type": "ARRAY", "items": { "type": "STRING" } } } } }, "kpMastery": { "type": "ARRAY", "items": { "type": "OBJECT", "properties": { "name": { "type": "STRING" }, "rate": { "type": "NUMBER" }, "errors": { "type": "ARRAY", "items": { "type": "STRING" } } } } }, "summaryRemarks": { "type": "ARRAY", "items": { "type": "STRING" } }, "stratification": { "type": "OBJECT", "properties": { "high": { "type": "ARRAY", "items": { "type": "STRING" } }, "medium": { "type": "ARRAY", "items": { "type": "STRING" } }, "low": { "type": "ARRAY", "items": { "type": "STRING" } } } }, "caseStudies": { "type": "ARRAY", "items": { "type": "OBJECT", "properties": { "name": { "type": "STRING" }, "score": { "type": "NUMBER" }, "problems": { "type": "ARRAY", "items": { "type": "STRING" } }, "strategies": { "type": "ARRAY", "items": { "type": "STRING" } } } } }, "caseSummary": { "type": "ARRAY", "items": { "type": "STRING" } } } };
async function callGeminiAPI(prompt) {
// ⚠️请在此处替换为你的Cloudflare Worker URL ⚠️
const workerUrl = "https://orange-frog-89ba.linxiaochun168.workers.dev/";
const userPassword = localStorage.getItem('app_password') || "";
if (!userPassword) { throw new Error("请先登录。"); }
const payload = { contents: [{ parts: [{ text: prompt }] }], generationConfig: { responseMimeType: "application/json", responseSchema: aiResponseSchema, temperature: 0.2 } };
let response;
try {
response = await fetch(workerUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': userPassword },
body: JSON.stringify(payload)
});
if (response.status === 401) { throw new Error("密码错误或已过期,请刷新页面重新登录。"); }
if (!response.ok) { throw new Error(`Server Error: ${response.status} ${response.statusText}`); }
const result = await response.json();
if (result.candidates && result.candidates.length > 0 && result.candidates[0].content) {
const text = result.candidates[0].content.parts[0].text;
return JSON.parse(text);
} else {
throw new Error("Invalid API response structure.");
}
} catch (error) {
console.error("Gemini API call failed:", error);
throw error;
}
}
async function callGeminiWithRetry(prompt, retries = 3, delay = 1000) { for (let i = 0; i < retries; i++) { try { return await callGeminiAPI(prompt); } catch (error) { if (i === retries - 1) throw error; await new Promise(res => setTimeout(res, delay * (i + 1))); } } }
async function startFullAnalysis() {
analysisButtonContainer.classList.add('hidden');
analysisErrorMessage.classList.add('hidden');
analysisLoading.classList.remove('hidden');
try {
let file1Promise = parseExcel(overallGradesFile);
let file2Promise = parseExcel(itemScoresFile);
let pdfTextPromise = parsePdfForText(paperPdfFile);
let [file1Data, file2Data, pdfText] = await Promise.all([file1Promise, file2Promise, pdfTextPromise]);
const isFile1ItemScore = file1Data.data.some(row => String(row[0]).trim().includes('得分率'));
const isFile2ItemScore = file2Data.data.some(row => String(row[0]).trim().includes('得分率'));
let overallGrades, itemScores;
if (isFile1ItemScore && !isFile2ItemScore) { overallGrades = file2Data; itemScores = file1Data; } else { overallGrades = file1Data; itemScores = file2Data; }
const analysisData = processStudentData(overallGrades, itemScores);
const threshold = parseInt(lowScoreBoundaryInput.value) || 60;
const aiPrompt = createAIPrompt(analysisData, pdfText, threshold);
const aiJson = await callGeminiWithRetry(aiPrompt);
populateReport(analysisData, aiJson);
analysisUploadStep.classList.add('hidden');
analysisLoading.classList.add('hidden');
analysisReportSection.classList.remove('hidden');
} catch (error) {
console.error("Analysis failed:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
analysisErrorMessage.classList.remove('hidden');
analysisErrorDetails.textContent = `${translations[currentLang].ta_error_details_prefix} ${errorMessage}`;
analysisLoading.classList.add('hidden');
analysisButtonContainer.classList.remove('hidden');
}
}
function populateReport(analysisData, aiJson) {
const { stats, distribution, className } = analysisData;
const { lowScoreAnalysis, kpMastery, summaryRemarks, stratification, caseStudies, caseSummary } = aiJson;
taReportTitle.textContent = `${className} ${translations[currentLang].ta_report_main_title}`;
uploadStatusList.innerHTML = `<li class="flex items-center gap-2"><svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.707-9.293a1 1 0 0 0-1.414-1.414L9 10.586 7.707 9.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4Z" clip-rule="evenodd" /></svg> <span>${overallGradesFile.name}</span></li><li class="flex items-center gap-2"><svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.707-9.293a1 1 0 0 0-1.414-1.414L9 10.586 7.707 9.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4Z" clip-rule="evenodd" /></svg> <span>${itemScoresFile.name}</span></li><li class="flex items-center gap-2"><svg class="h-5 w-5 text-green-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm3.707-9.293a1 1 0 0 0-1.414-1.414L9 10.586 7.707 9.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4Z" clip-rule="evenodd" /></svg> <span>${paperPdfFile.name}</span></li>`;
taStatStudents.textContent = stats.count; taStatAvg.textContent = stats.avg; taStatHighest.textContent = stats.highest; taStatLowest.textContent = stats.lowest; taStatStddev.textContent = stats.stdDev; taStatPassRate.textContent = stats.passRate;
distributionStatsTableBody.innerHTML = `<tr class="bg-white border-b"><td class="p-3 font-medium text-green-600">${distribution.exceeds}</td><td class="p-3 font-medium text-blue-600">${distribution.meets_plus}</td><td class="p-3 font-medium text-yellow-600">${distribution.meets}</td><td class="p-3 font-medium text-red-600">${distribution.working_toward}</td><td class="p-3 font-medium text-red-700">${distribution.try_again}</td></tr>`;
lowScoreCardContainer.innerHTML = lowScoreAnalysis.map(item => { const score = parseFloat(item.scoreRate); const bgColorClass = score < 35 ? 'bg-red-50' : 'bg-yellow-50'; const textColorClass = score < 35 ? 'text-red-600' : 'text-yellow-700'; return `<div class="border rounded-lg shadow-sm bg-white overflow-hidden"><div class="p-4 flex justify-between items-center border-b ${bgColorClass}"><div><span class="text-lg font-bold text-slate-800">${item.question || 'Unknown Q'}</span><span class="block text-sm font-semibold ${textColorClass}">${item.scoreRate || 'N/A'} <span data-lang-key="ta_low_q_header_rate">${translations[currentLang].ta_low_q_header_rate}</span></span></div><span class="font-semibold text-slate-700 text-right">${item.knowledgePoint || 'N/A'}</span></div><div class="p-4 space-y-3"><div><h5 class="font-medium text-slate-600" data-lang-key="ta_low_q_header_kp">${translations[currentLang].ta_low_q_header_kp}</h5><p class="text-sm text-slate-500 italic" title="${item.questionText || ''}">${item.questionText || 'N/A'}</p></div><div><h5 class="font-medium text-slate-600" data-lang-key="ta_low_q_header_error">${translations[currentLang].ta_low_q_header_error}</h5><p class="text-sm text-slate-700">${item.errorType || 'N/A'}</p></div><div><h5 class="font-medium text-slate-600" data-lang-key="ta_low_q_header_solution">${translations[currentLang].ta_low_q_header_solution}</h5><ul class="list-decimal pl-5 text-sm text-slate-700 space-y-1">${(item.solution || []).map(s => `<li>${s}</li>`).join('')}</ul></div></div></div>`; }).join('');
kpMasteryContainer.innerHTML = kpMastery.map(kp => { let ratePercent = parseFloat(kp.rate); if (ratePercent >= 0 && ratePercent <= 1) { ratePercent = ratePercent * 100; } const rateColor = ratePercent < 60 ? 'bg-red-500' : (ratePercent < 75 ? 'bg-yellow-500' : 'bg-green-500'); const errorList = (kp.errors || []).map(err => `<li class="text-sm">${err}</li>`).join(''); return `<div class="border rounded-lg p-4"><div class="flex justify-between items-center mb-2"><span class="font-semibold">${kp.name || 'N/A'}</span><span class="font-bold text-lg ${rateColor.replace('bg-', 'text-')}">${ratePercent.toFixed(1)}%</span></div><div class="w-full bg-slate-200 rounded-full h-2.5"><div class="${rateColor} h-2.5 rounded-full" style="width: ${ratePercent}%"></div></div><div class="mt-3"><h5 class="font-medium text-slate-600" data-lang-key="ta_low_q_header_error">${translations[currentLang].ta_low_q_header_error}:</h5><ul class="list-disc pl-5 mt-1 text-slate-500">${errorList}</ul></div></div>`; }).join('');
summaryRemarksContent.innerHTML = `<ul>${(summaryRemarks || []).map(s => `<li>${s}</li>`).join('')}</ul>`;
stratificationContainer.innerHTML = `<div class="border-l-4 border-green-500 bg-white p-4 rounded-r-lg shadow"><h4 class="text-lg font-bold text-green-600" data-lang-key="ta_dist_exceeds">${translations[currentLang].ta_dist_exceeds}</h4><ul>${(stratification.high || []).map(s => `<li>${s}</li>`).join('')}</ul></div><div class="border-l-4 border-yellow-500 bg-white p-4 rounded-r-lg shadow"><h4 class="text-lg font-bold text-yellow-600" data-lang-key="ta_dist_meets">${translations[currentLang].ta_dist_meets}</h4><ul>${(stratification.medium || []).map(s => `<li>${s}</li>`).join('')}</ul></div><div class="border-l-4 border-red-500 bg-white p-4 rounded-r-lg shadow"><h4 class="text-lg font-bold text-red-600" data-lang-key="ta_dist_working">${translations[currentLang].ta_dist_working}</h4><ul>${(stratification.low || []).map(s => `<li>${s}</li>`).join('')}</ul></div>`;
caseStudyContainer.innerHTML = caseStudies.map((student, index) => `<div class="border rounded-lg p-4"><div class="flex justify-between items-center border-b pb-2"><h4 class="text-lg font-semibold">${index + 1}. ${student.name}</h4><span class="text-2xl font-bold text-red-600">${student.score} <span data-lang-key="entry_student_score">${translations[currentLang].entry_student_score}</span></span></div><div class="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4"><div><h5 class="font-medium" data-lang-key="ta_case_study_issues">${translations[currentLang].ta_case_study_issues}:</h5><ul>${(student.problems || []).map(s => `<li>${s}</li>`).join('')}</ul></div><div><h5 class="font-medium" data-lang-key="ta_case_study_strategies">${translations[currentLang].ta_case_study_strategies}:</h5><ul>${(student.strategies || []).map(s => `<li>${s}</li>`).join('')}</ul></div></div></div>`).join('');
caseSummaryContent.innerHTML = `<ul>${(caseSummary || []).map(s => `<li>${s}</li>`).join('')}</ul>`;
}
// --- ADDED MISSING FUNCTIONS ---
function downloadReport() {
const reportHtml = analysisReportSection.innerHTML;
const styles = Array.from(document.styleSheets)
.map(styleSheet => {
try {
return Array.from(styleSheet.cssRules)
.map(rule => rule.cssText)
.join('\n');
} catch (e) {
return '';
}
})
.join('\n');
const tailwind = document.querySelector('script[src*="tailwindcss.com"]').outerHTML;
const fullHtml = `
<!DOCTYPE html>
<html lang="${currentLang}">
<head>
<meta charset="UTF-8">
<title>${taReportTitle.textContent}</title>
${tailwind}
<style>
body { font-family: 'Inter', sans-serif; padding: 2rem; background-color: #f1f5f9; }
.report-section { display: block !important; }
/* Re-add prose styles for lists */
.prose ul { list-style-position: outside; padding-left: 1.25rem; }
.prose li { margin-top: 0.5em; }
.prose h5 { font-size: 1.1em; font-weight: 600; margin-top: 1em; }
.report-section ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-top: 0.5rem;
}
</style>
</head>
<body class="bg-slate-100">
<main class="space-y-8 report-section">${reportHtml}</main>
</body>
</html>
`;
const blob = new Blob([fullHtml], { type: 'text/html' });
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = `${taReportTitle.textContent}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
}
function downloadWordReport() {
const title = translations[currentLang].ta_report_main_title;
const tailwind = document.querySelector('script[src*="tailwindcss.com"]').outerHTML;
// Helper function to get outerHTML of a section by its title's data-lang-key
const getSectionHtml = (key) => {
const el = document.querySelector(`[data-lang-key="${key}"]`);
if (el) {
// Find the closest parent container of the section
return el.closest('.p-6').outerHTML;
}
return '';
};
const fullHtml = `
<!DOCTYPE html>
<html lang="${currentLang}">
<head>
<meta charset="utf-8">
<!-- Specific settings for Word -->
<meta name=ProgId content=Word.Document>
<meta name=Generator content="Microsoft Word 15">
<meta name=Originator content="Microsoft Word 15">
<style>
body { font-family: 'Inter', sans-serif; padding: 2rem; background-color: #ffffff; }
.report-section { display: block !important; }
/* Page break avoidance */
.p-6, .border-l-4, .border, .border-l-4, .bg-white, .shadow { page-break-inside: avoid; }
/* Re-add prose styles */
.prose ul { list-style-position: outside; padding-left: 1.25rem; margin-top: 0.5em; }
.prose li { margin-top: 0.5em; }
.prose h5 { font-size: 1.1em; font-weight: 600; margin-top: 1em; }
.report-section ul {
list-style-type: disc;
padding-left: 1.5rem;
margin-top: 0.5rem;
}
</style>
${tailwind}
</head>
<body class="bg-white">
<!-- Add the new title -->
<h1 class="text-2xl font-bold text-slate-800">${title}</h1>
<!-- Add all sections *except* status -->
<main class="space-y-8 report-section">
${getSectionHtml('ta_report_basic_title')}
${getSectionHtml('ta_report_distribution_title')}
${getSectionHtml('ta_report_low_score_title')}
${getSectionHtml('ta_report_kp_mastery_title')}
${getSectionHtml('ta_report_summary_title')}
${getSectionHtml('ta_report_stratification_title')}
${getSectionHtml('ta_report_case_study_title')}
${getSectionHtml('ta_report_case_summary_title')}
</main>
</body>
</html>
`;
// Add BOM for UTF-8
const blob = new Blob(['\ufeff', fullHtml], {
type: 'application/msword;charset=utf-8'
});
const href = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = href;
a.download = `${title}.doc`; // Use .doc extension
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(href);
}
// --- EVENT LISTENERS ---
loginForm.addEventListener('submit', handleLogin);
navLinks.forEach(link => link.addEventListener('click', handleNavigation));
addScoreForm.addEventListener('submit', (e) => { e.preventDefault(); addScore(studentNameInput.value, studentScoreInput.value); addScoreForm.reset(); studentNameInput.focus(); });
scoresTableBody.addEventListener('click', (e) => { const removeBtn = e.target.closest('.remove-btn'); if (removeBtn) { removeScore(removeBtn.dataset.index); } });
fileUpload.addEventListener('change', (e) => handleFile(e.target.files[0]));
dropArea.addEventListener('dragover', (e) => { e.preventDefault(); dropArea.classList.add('border-blue-500', 'bg-blue-50'); });
dropArea.addEventListener('dragleave', () => dropArea.classList.remove('border-blue-500', 'bg-blue-50'));
dropArea.addEventListener('drop', (e) => { e.preventDefault(); dropArea.classList.remove('border-blue-500', 'bg-blue-50'); handleFile(e.dataTransfer.files[0]); });
analyzeBtn.addEventListener('click', () => { generateReport(); reportContainer.scrollIntoView({ behavior: 'smooth' }); });
clearBtn.addEventListener('click', () => showConfirmationModal('confirm_clear_title', 'confirm_clear_text', () => { studentData = []; currentClassName = ''; updateTable(); reportContainer.classList.add('hidden'); welcomeMessage.classList.remove('hidden'); }));
exportBtn.addEventListener('click', exportToCsv);
langEnBtn.addEventListener('click', () => setLanguage('en'));
langZhBtn.addEventListener('click', () => setLanguage('zh'));
modalCancelBtn.addEventListener('click', hideConfirmationModal);
setupFileUploader(overallGradesDropArea, overallGradesUpload, overallGradesFeedback, (file) => { overallGradesFile = file; });
setupFileUploader(itemScoresDropArea, itemScoresUpload, itemScoresFeedback, (file) => { itemScoresFile = file; });
setupFileUploader(paperPdfDropArea, paperPdfUpload, paperPdfFeedback, (file) => { paperPdfFile = file; });
startAnalysisBtn.addEventListener('click', startFullAnalysis);
downloadReportBtn.addEventListener('click', downloadReport);
downloadWordBtn.addEventListener('click', downloadWordReport);
updateTable();
setLanguage('zh');
document.querySelector('.nav-link[href="#grade-entry"]').classList.add('active');
dashboardSection.classList.remove('hidden');
gradeEntrySection.classList.remove('hidden');
});
</script>
</body>
</html>