mathwrite / templates /index.html
lexlepty's picture
Update templates/index.html
3c679aa verified
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>题目识别与解答系统(带公式识别与渲染)</title>
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet">
<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
<script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<style>
:root {
--primary: #4F46E5;
--primary-hover: #4338CA;
--secondary: #E0E7FF;
--text: #1F2937;
--background: #F9FAFB;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: var(--background);
color: var(--text);
}
.card {
background: white;
border-radius: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.btn {
background-color: var(--primary);
color: white;
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
transition: all 0.2s;
font-weight: 500;
}
.btn:hover {
background-color: var(--primary-hover);
transform: translateY(-1px);
}
.btn:active {
transform: translateY(0);
}
.btn-secondary {
background-color: var(--secondary);
color: var(--primary);
}
.btn-secondary:hover {
background-color: #D1D5DB;
}
.loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.9);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid var(--secondary);
border-top: 5px solid var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.preview-container {
aspect-ratio: 16/9;
overflow: hidden;
border-radius: 0.5rem;
background-color: var(--background);
border: 2px dashed #E5E7EB;
}
.preview-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.solution-box {
border: 1px solid #E5E7EB;
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 1rem;
background-color: #FFFFFF;
}
.solution-box h3 {
color: var(--primary);
margin-bottom: 0.5rem;
}
.copy-btn {
position: absolute;
right: 1rem;
top: 1rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
opacity: 0;
transition: opacity 0.2s;
}
.solution-box:hover .copy-btn {
opacity: 1;
}
.prose {
font-size: 1rem;
line-height: 1.75;
color: var(--text);
}
.prose p {
margin-bottom: 1.25em;
}
.prose .math {
overflow-x: auto;
margin: 1em 0;
}
#solution-content,
#analysis-content {
opacity: 0;
transition: opacity 0.3s ease;
}
.solution-box {
position: relative;
margin-bottom: 1.5rem;
padding: 1.5rem;
}
</style>
</head>
<body>
<div class="loading">
<div class="spinner"></div>
</div>
<div class="container mx-auto px-4 py-8 max-w-6xl">
<h1 class="text-4xl font-bold text-center mb-8 text-gray-900">题目识别与解答系统(带公式识别与渲染)</h1>
<!-- 上传部分 -->
<div class="card p-6 mb-8">
<h2 class="text-2xl font-semibold mb-4">图片上传</h2>
<div class="space-y-4">
<input type="file"
id="image-input"
accept="image/*"
class="hidden"
onchange="handleImageUpload(event)">
<label for="image-input"
class="btn inline-block cursor-pointer">
选择图片
</label>
<div class="preview-container">
<img id="preview-image"
class="preview-image"
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="
alt="预览">
</div>
<button onclick="processImage()" class="btn w-full">
开始识别
</button>
</div>
</div>
<!-- 识别结果 -->
<div id="result-section" class="card p-6 mb-8" style="display: none;">
<h2 class="text-2xl font-semibold mb-4">识别结果</h2>
<div class="grid md:grid-cols-2 gap-6">
<div>
<h3 class="text-lg font-medium mb-2">源代码</h3>
<textarea id="source-text"
class="w-full h-64 p-4 border rounded-lg resize-y font-mono text-sm"
readonly></textarea>
<div class="flex gap-2 mt-2">
<button onclick="copyContent('text')" class="btn">复制文本</button>
<button onclick="copyContent('formulas')" class="btn">复制公式</button>
<button onclick="copyContent('all')" class="btn">复制全部</button>
</div>
</div>
<div>
<h3 class="text-lg font-medium mb-2">预览</h3>
<div id="preview-text"
class="w-full h-64 p-4 border rounded-lg overflow-y-auto bg-white"></div>
</div>
</div>
<div class="mt-6">
<button onclick="getSolution()" class="btn w-full">
获取解答
</button>
</div>
</div>
<!-- 解答部分的 HTML -->
<!-- 解答部分的样式 -->
<style>
.math-solution-text {
font-size: 1rem;
line-height: 1.75;
color: #1F2937;
background-color: white;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
margin-top: 0.5rem;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid #E5E7EB;
min-height: 2rem;
}
.math-solution-text .math {
display: inline-block;
margin: 0.5em 0;
overflow-x: auto;
max-width: 100%;
}
.math-solution-text p {
margin-bottom: 1em;
}
</style>
<!-- 解答部分的 HTML -->
<div id="solution-section" class="card p-6" style="display: none;">
<!-- 答案部分 -->
<div class="solution-box relative mb-4">
<h3 class="text-xl font-medium">答案</h3>
<button onclick="copySolutionContent('answer')"
class="copy-btn btn-secondary">
复制答案
</button>
<div id="answer-content" class="math-solution-text"></div>
</div>
<!-- 解析部分 -->
<div class="solution-box relative">
<h3 class="text-xl font-medium">解析</h3>
<button onclick="copySolutionContent('analysis')"
class="copy-btn btn-secondary">
复制解析
</button>
<div id="analysis-content" class="math-solution-text"></div>
</div>
</div>
</div>
<script>
let currentResult = null;
let originalLatexContent = '';
function toggleLoading(show) {
document.querySelector('.loading').style.display = show ? 'flex' : 'none';
}
function showToast(message, type = 'info') {
alert(message); // 简单起见使用alert
}
function handleImageUpload(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
document.getElementById('preview-image').src = e.target.result;
};
reader.readAsDataURL(file);
}
async function processImage() {
const fileInput = document.getElementById('image-input');
const file = fileInput.files[0];
if (!file) {
showToast('请先选择图片', 'error');
return;
}
toggleLoading(true);
try {
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/process', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error('处理失败');
}
const result = await response.json();
if (result.error) {
throw new Error(result.error);
}
currentResult = result.result;
// 更新预览图片
document.getElementById('preview-image').src =
`data:image/png;base64,${result.original_image}`;
// 更新源代码视图
document.getElementById('source-text').value =
JSON.stringify(currentResult, null, 2);
// 保存原始带LaTeX的内容
originalLatexContent = currentResult.text;
currentResult.formulas.forEach((formula, index) => {
originalLatexContent = originalLatexContent.replace(
`[formula_${index + 1}]`,
`$${formula}$`
);
});
// 更新预览视图
const previewDiv = document.getElementById('preview-text');
previewDiv.innerHTML = originalLatexContent;
MathJax.typesetPromise([previewDiv]).catch(console.error);
document.getElementById('result-section').style.display = 'block';
showToast('识别成功');
} catch (error) {
showToast(error.message, 'error');
console.error('处理错误:', error);
} finally {
toggleLoading(false);
}
}
async function copyContent(type) {
if (!currentResult) return;
try {
let content = '';
switch (type) {
case 'text':
content = currentResult.text;
break;
case 'formulas':
content = currentResult.formulas.join('\n');
break;
case 'all':
content = originalLatexContent;
break;
}
await navigator.clipboard.writeText(content);
showToast('复制成功');
} catch (err) {
showToast('复制失败', 'error');
console.error('复制错误:', err);
}
}
async function copySolutionContent(type) {
const element = document.getElementById(`${type}-content`);
if (!element) return;
try {
const content = element.getAttribute('data-original') || element.textContent;
await navigator.clipboard.writeText(content);
showToast('复制成功');
} catch (err) {
showToast('复制失败', 'error');
console.error('复制错误:', err);
}
}
async function getSolution() {
if (!currentResult) {
showToast('没有可解答的内容', 'error');
return;
}
toggleLoading(true);
const solutionSection = document.getElementById('solution-section');
const answerContent = document.getElementById('answer-content');
const analysisContent = document.getElementById('analysis-content');
solutionSection.style.display = 'block';
answerContent.innerHTML = '';
analysisContent.innerHTML = '';
try {
const response = await fetch('/solve', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
text: currentResult.text,
formulas: currentResult.formulas
})
});
if (!response.ok) throw new Error('获取解答失败');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let answer = '';
let analysis = '';
while (true) {
const {value, done} = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (!line.trim() || !line.startsWith('data: ')) continue;
const data = JSON.parse(line.slice(5));
if (data.content) {
if (data.type === 'answer') {
// 将 $$ 替换为 \[ 和 \]
answer += data.content.replace(/\$\$(.*?)\$\$/g, '\\[$1\\]');
answerContent.innerHTML = answer;
answerContent.setAttribute('data-original', answer);
await MathJax.typesetPromise([answerContent]);
} else if (data.type === 'analysis') {
// 直接使用内容,保持 $ 符号的处理方式与预览一致
analysis += data.content;
analysisContent.innerHTML = analysis;
analysisContent.setAttribute('data-original', analysis);
await MathJax.typesetPromise([analysisContent]);
analysisContent.style.opacity = '1';
}
}
}
}
} catch (error) {
showToast(error.message, 'error');
answerContent.innerHTML = `<p class="text-red-500">获取解答失败: ${error.message}</p>`;
} finally {
toggleLoading(false);
}
}
async function copySolutionContent(type) {
const element = document.getElementById(`${type}-content`);
if (!element) return;
try {
const content = element.getAttribute('data-original');
if (content) {
await navigator.clipboard.writeText(content);
showToast('复制成功');
// 添加复制成功的动画效果
const button = element.previousElementSibling;
const originalText = button.textContent;
button.textContent = '复制成功';
button.classList.add('bg-green-100');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('bg-green-100');
}, 1000);
}
} catch (err) {
showToast('复制失败', 'error');
console.error('复制错误:', err);
}
}
window.MathJax = {
tex: {
inlineMath: [['$', '$'], ['\\(', '\\)']],
displayMath: [['$$', '$$'], ['\\[', '\\]']],
processEscapes: true
},
options: {
ignoreHtmlClass: 'tex2jax_ignore',
processHtmlClass: 'tex2jax_process'
},
startup: {
pageReady() {
return MathJax.startup.defaultPageReady();
}
}
};
// 添加复制成功的动画效果
function addCopyAnimation(button) {
const originalText = button.textContent;
button.textContent = '已复制';
button.classList.add('bg-green-500');
setTimeout(() => {
button.textContent = originalText;
button.classList.remove('bg-green-500');
}, 1000);
}
// 优化解答和解析的展示逻辑
function updateSolutionDisplay(content, type) {
const container = document.getElementById(`${type}-content`);
if (!container) return;
// 保存原始内容用于复制
const originalContent = content;
container.setAttribute('data-original', originalContent);
// 渲染内容
container.innerHTML = content;
// 渲染数学公式
MathJax.typesetPromise([container]).then(() => {
// 添加滑入动画
container.style.opacity = '0';
container.style.transform = 'translateY(20px)';
container.style.transition = 'all 0.5s ease';
requestAnimationFrame(() => {
container.style.opacity = '1';
container.style.transform = 'translateY(0)';
});
}).catch(console.error);
}
// 优化错误处理
function handleError(error, container) {
const errorMessage = document.createElement('div');
errorMessage.className = 'bg-red-50 border-l-4 border-red-500 p-4 my-4';
errorMessage.innerHTML = `
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
</div>
<div class="ml-3">
<p class="text-sm text-red-700">
${error.message || '发生错误,请稍后重试'}
</p>
</div>
</div>
`;
container.appendChild(errorMessage);
}
// 全局错误处理
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error);
showToast('操作出错,请刷新页面重试', 'error');
});
// 优化图片上传体验
document.getElementById('image-input').addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
// 验证文件类型
if (!file.type.startsWith('image/')) {
showToast('请选择图片文件', 'error');
event.target.value = '';
return;
}
// 验证文件大小(最大10MB)
if (file.size > 10 * 1024 * 1024) {
showToast('图片大小不能超过10MB', 'error');
event.target.value = '';
return;
}
handleImageUpload(event);
});
</script>
</body>
</html>