Download templates/index.html from lexlepty/mathwrite: direct link, hf CLI and curl.
- Browser
- Download file 19.9 kB
-
https://huggingface.co/spaces/lexlepty/mathwrite/resolve/main/templates/index.html
- Command line
-
hf download hf://spaces/lexlepty/mathwrite/templates/index.html
-
curl -L -o index.html https://huggingface.co/spaces/lexlepty/mathwrite/resolve/main/templates/index.html
19.9 kB
| <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/tailwindcss@2.2.19/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> |