6 Commits

Author SHA1 Message Date
liuyuanchuang
e90fca5ab1 feat: add robots 2026-02-25 15:47:23 +08:00
liuyuanchuang
bc4b547e03 Merge branch 'main' of https://code.texpixel.com/YogeLiu/doc_ai_frontend 2026-02-06 22:33:58 +08:00
liuyuanchuang
e4c6a09cf8 feat: rm dist 2026-02-05 18:23:17 +08:00
liuyuanchuang
2b1da79bbc feat: add toast for no content 2026-02-05 18:22:30 +08:00
564aaec581 Merge pull request 'feat: add track point && rm omml' (#1) from test into main
Reviewed-on: #1
2026-02-05 13:49:19 +08:00
liuyuanchuang
d562d67203 feat: add track point 2026-01-27 23:44:45 +08:00
24 changed files with 1143 additions and 113 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ dist-ssr
*.sw?
.env
/dist
app.cloud/

202
deploy_dev.sh Executable file
View File

@@ -0,0 +1,202 @@
#!/bin/bash
# Document AI Frontend 部署脚本
# 功能:构建项目并部署到 ubuntu 服务器
set -e # 遇到错误立即退出
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 服务器配置
ubuntu_HOST="ubuntu"
DEPLOY_PATH="/var/www"
DEPLOY_NAME="app.cloud"
# 打印带颜色的消息
print_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# 检查命令是否存在
check_command() {
if ! command -v $1 &> /dev/null; then
print_error "$1 命令未找到,请先安装"
exit 1
fi
}
# 部署到服务器
deploy_to_server() {
local server=$1
print_info "开始部署到 ${server}..."
# 上传构建产物app.cloud 目录)
print_info "上传 ${DEPLOY_NAME} 目录到 ${server}..."
scp_output=$(scp -r ${DEPLOY_NAME} ${server}:~ 2>&1)
scp_exit_code=$?
if [ $scp_exit_code -eq 0 ]; then
print_success "文件上传成功"
else
print_error "文件上传失败,请检查 SSH 连接和权限"
echo "$scp_output" | sed 's/^/ /'
return 1
fi
# SSH 执行部署操作(非交互模式)
print_info "${server} 上执行部署操作..."
print_info "部署路径: ${DEPLOY_PATH}/${DEPLOY_NAME}"
ssh_output=$(ssh ${server} bash << SSH_EOF
set -e
DEPLOY_PATH="${DEPLOY_PATH}"
DEPLOY_NAME="${DEPLOY_NAME}"
# 检查部署目录是否存在
if [ ! -d "\${DEPLOY_PATH}" ]; then
echo "错误:部署目录 \${DEPLOY_PATH} 不存在,请检查路径是否正确"
exit 1
fi
# 检查是否有权限写入,若无则尝试免密 sudosudo -n
SUDO_CMD=""
if ! touch "\${DEPLOY_PATH}/.deploy_test" 2>/dev/null; then
if sudo -n true 2>/dev/null; then
echo "提示:没有直接写入权限,使用 sudo -n 执行部署操作"
SUDO_CMD="sudo -n"
else
echo "错误:没有写入权限,且 sudo 需要密码(非交互部署无法输入)"
echo "请执行以下任一方案后重试:"
echo " 1) 将部署目录改为当前用户可写目录(例如 /home/\$USER/www"
echo " 2) 为当前用户配置免密 sudoNOPASSWD"
exit 1
fi
else
rm -f "\${DEPLOY_PATH}/.deploy_test"
echo "提示:检测到部署目录可直接写入"
fi
# 备份旧版本(如果存在)
if [ -d "\${DEPLOY_PATH}/\${DEPLOY_NAME}" ]; then
echo "备份旧版本..."
\$SUDO_CMD rm -rf "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" 2>/dev/null || true
\$SUDO_CMD mv "\${DEPLOY_PATH}/\${DEPLOY_NAME}" "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" || { echo "错误:备份失败,权限不足"; exit 1; }
fi
# 移动新版本到部署目录(覆盖现有目录)
if [ -d ~/\${DEPLOY_NAME} ]; then
echo "移动新版本到部署目录..."
\$SUDO_CMD mv ~/\${DEPLOY_NAME} "\${DEPLOY_PATH}/" || { echo "错误:移动文件失败,权限不足"; exit 1; }
echo "部署完成!"
else
echo "错误:找不到 ~/\${DEPLOY_NAME} 目录"
exit 1
fi
# 重新加载 nginx如果配置了
if command -v nginx &> /dev/null; then
echo "重新加载 nginx..."
if [ -n "\$SUDO_CMD" ]; then
\$SUDO_CMD nginx -t && \$SUDO_CMD nginx -s reload || echo "警告nginx 重新加载失败,请手动检查"
else
nginx -t && nginx -s reload || echo "警告nginx 重新加载失败,请手动检查"
fi
fi
SSH_EOF
)
ssh_exit_code=$?
# 显示 SSH 输出
if [ -n "$ssh_output" ]; then
echo "$ssh_output" | sed 's/^/ /'
fi
if [ $ssh_exit_code -eq 0 ]; then
print_success "${server} 部署成功!"
else
print_error "${server} 部署失败!"
print_error "请检查:"
print_error " 1. SSH 连接是否正常"
print_error " 2. 部署目录 ${DEPLOY_PATH} 是否存在"
print_error " 3. 是否有 sudo 权限(如果需要)"
print_error " 4. 上传的文件 ~/${DEPLOY_NAME} 是否存在"
return 1
fi
}
# 主函数
main() {
print_info "=========================================="
print_info "Document AI Frontend 部署脚本"
print_info "=========================================="
echo ""
# 检查必要的命令
print_info "检查环境..."
check_command "npm"
check_command "scp"
check_command "ssh"
# 步骤1: 构建项目
print_info "步骤 1/2: 构建项目(测试环境)..."
if npm run build:dev; then
print_success "构建完成!"
else
print_error "构建失败!"
exit 1
fi
echo ""
# 检查 dist 目录是否存在
if [ ! -d "dist" ]; then
print_error "dist 目录不存在,构建可能失败"
exit 1
fi
# 重命名 dist 为 app.cloud
print_info "重命名 dist 为 ${DEPLOY_NAME}..."
if [ -d "${DEPLOY_NAME}" ]; then
rm -rf "${DEPLOY_NAME}"
fi
mv dist "${DEPLOY_NAME}"
print_success "重命名完成"
echo ""
# 步骤2: 部署到 ubuntu
print_info "步骤 2/2: 部署到 ubuntu..."
if deploy_to_server ${ubuntu_HOST}; then
print_success "ubuntu 部署完成"
else
print_error "ubuntu 部署失败"
exit 1
fi
echo ""
# 完成
print_info "清理临时文件..."
# 可以选择是否删除本地的 app.cloud 目录
# rm -rf ${DEPLOY_NAME}
print_success "=========================================="
print_success "部署完成!"
print_success "=========================================="
}
# 运行主函数
main

View File

@@ -106,8 +106,8 @@ main() {
check_command "ssh"
# 步骤1: 构建项目
print_info "步骤 1/2: 构建项目..."
if npm run build; then
print_info "步骤 1/2: 构建项目(生产环境)..."
if npm run build:prod; then
print_success "构建完成!"
else
print_error "构建失败!"

View File

@@ -0,0 +1,140 @@
# 多语言功能实现总结
## 📋 实现内容
### 1. HTML 文档改进 (`index.html`)
- ✅ 添加 `hreflang` 标签支持多语言 SEO
- ✅ 添加双语 meta 标签description, keywords
- ✅ 添加 Open Graph 和 Twitter Cards 多语言支持
- ✅ 添加动态语言检测脚本
- ✅ 优化 `og:locale``og:locale:alternate` 标签
### 2. SEO 辅助工具 (`src/lib/seoHelper.ts`)
- ✅ 创建 `updatePageMeta()` 函数动态更新 meta 标签
- ✅ 创建 `getSEOContent()` 函数获取语言特定的 SEO 内容
- ✅ 支持更新标题、描述、关键词、OG 标签等
### 3. 语言上下文增强 (`src/contexts/LanguageContext.tsx`)
- ✅ 集成 `updatePageMeta()` 在语言切换时自动更新页面
- ✅ 在初始加载时根据用户偏好更新 meta 标签
- ✅ 在 IP 检测后更新 meta 标签
### 4. 文档和测试
- ✅ 创建详细的多语言支持文档 (`MULTILANG_README.md`)
- ✅ 创建浏览器测试脚本 (`test-multilang.js`)
- ✅ 包含最佳实践和使用示例
## 🎯 功能特性
### 自动语言检测优先级
1. 用户在 localStorage 中保存的选择(最高优先级)
2. IP 地理位置检测
3. 浏览器语言设置(回退)
### SEO 优化
- **搜索引擎**: 完整的 hreflang 支持
- **社交媒体**: Open Graph 和 Twitter Cards
- **元数据**: 动态更新标题、描述、关键词
### 用户体验
- 一键语言切换(在导航栏)
- 无刷新页面更新
- 保持用户选择跨会话
## 📝 使用示例
### 切换语言
```typescript
import { useLanguage } from './contexts/LanguageContext';
function Component() {
const { language, setLanguage, t } = useLanguage();
return (
<button onClick={() => setLanguage('en')}>
{t.common.switchLanguage}
</button>
);
}
```
### 添加新翻译
`src/lib/translations.ts` 中:
```typescript
export const translations = {
en: {
newFeature: {
title: 'New Feature',
}
},
zh: {
newFeature: {
title: '新功能',
}
}
};
```
### 更新 SEO 内容
`src/lib/seoHelper.ts` 中:
```typescript
const seoContent: Record<Language, SEOContent> = {
en: {
title: 'Your English Title',
description: 'Your English description',
keywords: 'english, keywords',
},
zh: {
title: '您的中文标题',
description: '您的中文描述',
keywords: '中文,关键词',
},
};
```
## 🧪 测试清单
- [ ] 页面首次加载时语言检测正确
- [ ] 切换语言时标题更新
- [ ] 切换语言时 meta 标签更新
- [ ] HTML lang 属性同步更新
- [ ] localStorage 正确保存用户选择
- [ ] 所有 UI 文本正确翻译
- [ ] Open Graph 预览正确显示(使用 [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
- [ ] Twitter Card 预览正确显示(使用 [Twitter Card Validator](https://cards-dev.twitter.com/validator)
## 🚀 部署注意事项
### 服务器配置
如果使用基于 URL 的语言路由(如 `/en`, `/zh`),需要配置服务器重写规则:
**Nginx 示例**:
```nginx
location ~ ^/(en|zh) {
try_files $uri $uri/ /index.html;
}
```
**Apache 示例**:
```apache
RewriteEngine On
RewriteRule ^(en|zh)/ /index.html [L]
```
### CDN 缓存
确保 CDN 不会缓存带有错误语言的页面:
- 使用 `Vary: Accept-Language`
- 或在 URL 中包含语言参数
## 🔗 相关资源
- [Google 国际化指南](https://developers.google.com/search/docs/specialty/international/localized-versions)
- [Open Graph Protocol](https://ogp.me/)
- [Twitter Cards 文档](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards)
- [hreflang 最佳实践](https://support.google.com/webmasters/answer/189077)
## 📞 支持
如有问题或建议,请联系:
- Email: yogecoder@gmail.com
- QQ 群: 1018282100

174
docs/MULTILANG_README.md Normal file
View File

@@ -0,0 +1,174 @@
# 多语言支持说明 / Multi-language Support
## 概述 / Overview
TexPixel 现在支持完整的中英文双语切换,包括:
- 动态页面标题和 meta 标签更新
- SEO 优化的多语言支持
- 智能语言检测(基于 IP 和浏览器偏好)
TexPixel now supports complete bilingual switching between Chinese and English, including:
- Dynamic page title and meta tag updates
- SEO-optimized multi-language support
- Intelligent language detection (based on IP and browser preferences)
---
## 功能特性 / Features
### 1. 自动语言检测 / Automatic Language Detection
应用会按以下优先级检测用户语言:
1. localStorage 中保存的用户选择
2. IP 地理位置检测
3. 浏览器语言设置
The app detects user language in the following order of priority:
1. User's saved preference in localStorage
2. IP geolocation detection
3. Browser language settings
### 2. SEO 优化 / SEO Optimization
- **hreflang 标签**:告知搜索引擎不同语言版本的页面
- **多语言 meta 标签**description、keywords、og:locale 等
- **动态标题更新**:切换语言时自动更新页面标题
Features include:
- **hreflang tags**: Inform search engines about different language versions
- **Multilingual meta tags**: description, keywords, og:locale, etc.
- **Dynamic title updates**: Automatically update page title when switching languages
### 3. Open Graph 和 Twitter Cards
支持社交媒体分享的多语言 meta 标签:
- Facebook (Open Graph)
- Twitter Cards
- 其他支持 OG 协议的平台
Multilingual meta tags for social media sharing:
- Facebook (Open Graph)
- Twitter Cards
- Other platforms supporting OG protocol
---
## 技术实现 / Technical Implementation
### 文件结构 / File Structure
```
src/
├── lib/
│ ├── seoHelper.ts # SEO 元数据管理 / SEO metadata management
│ ├── translations.ts # 翻译文本 / Translation texts
│ └── ipLocation.ts # IP 定位 / IP location detection
├── contexts/
│ └── LanguageContext.tsx # 语言上下文 / Language context
└── components/
└── Navbar.tsx # 语言切换器 / Language switcher
```
### 核心函数 / Core Functions
#### `updatePageMeta(language: Language)`
更新页面的所有 SEO 相关元数据,包括:
- document.title
- HTML lang 属性
- meta description
- meta keywords
- Open Graph 标签
- Twitter Card 标签
Updates all SEO-related metadata on the page, including:
- document.title
- HTML lang attribute
- meta description
- meta keywords
- Open Graph tags
- Twitter Card tags
#### `getSEOContent(language: Language)`
获取指定语言的 SEO 内容(标题、描述、关键词)
Get SEO content for a specific language (title, description, keywords)
---
## 使用方法 / Usage
### 在组件中使用语言功能 / Using Language Features in Components
```typescript
import { useLanguage } from '../contexts/LanguageContext';
function MyComponent() {
const { language, setLanguage, t } = useLanguage();
return (
<div>
<h1>{t.common.title}</h1>
<button onClick={() => setLanguage('en')}>English</button>
<button onClick={() => setLanguage('zh')}></button>
</div>
);
}
```
### 添加新的翻译文本 / Adding New Translation Texts
`src/lib/translations.ts` 中添加:
```typescript
export const translations = {
en: {
myFeature: {
title: 'My Feature',
description: 'Feature description',
}
},
zh: {
myFeature: {
title: '我的功能',
description: '功能描述',
}
}
};
```
---
## 最佳实践 / Best Practices
1. **始终提供双语内容** / Always provide bilingual content
- 确保所有用户可见的文本都有中英文翻译
- Ensure all user-visible text has Chinese and English translations
2. **保持 SEO 元数据最新** / Keep SEO metadata up-to-date
-`seoHelper.ts` 中维护准确的页面描述和关键词
- Maintain accurate page descriptions and keywords in `seoHelper.ts`
3. **测试语言切换** / Test language switching
- 确保切换语言时页面标题和 meta 标签正确更新
- Ensure page title and meta tags update correctly when switching languages
4. **考虑 RTL 语言** / Consider RTL languages
- 虽然目前只支持中英文,但代码架构支持未来添加其他语言
- While currently supporting only Chinese and English, the architecture supports adding other languages in the future
---
## 路线图 / Roadmap
- [ ] 添加更多语言支持(日语、韩语等)/ Add more language support (Japanese, Korean, etc.)
- [ ] 实现 URL 路由多语言 (/en, /zh) / Implement URL routing for languages
- [ ] 服务端渲染 (SSR) 支持 / Server-side rendering (SSR) support
- [ ] 语言特定的日期和数字格式化 / Language-specific date and number formatting
---
## 相关链接 / Related Links
- [hreflang 标签最佳实践](https://developers.google.com/search/docs/specialty/international/localized-versions)
- [Open Graph Protocol](https://ogp.me/)
- [Twitter Cards Documentation](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards)

View File

@@ -1,35 +1,89 @@
<!doctype html>
<html lang="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>⚡️ TexPixel - 公式识别工具</title>
<link rel="canonical" href="https://texpixel.com/" />
<link rel="alternate" hreflang="zh-CN" href="https://texpixel.com/" />
<link rel="alternate" hreflang="en" href="https://texpixel.com/en/" />
<link rel="alternate" hreflang="x-default" href="https://texpixel.com/" />
<!-- Dynamic Title (will be updated by app) -->
<title>⚡️ TexPixel - 公式识别工具 | Formula Recognition Tool</title>
<!-- SEO Meta Tags -->
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
<!-- SEO Meta Tags - Chinese (Default) -->
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。Online formula recognition tool supporting printed and handwritten math formulas." />
<meta name="keywords"
content="公式识别,数学公式,OCR,手写公式识别,印刷体识别,AI识别,数学工具,免费,handwriting,formula,recognition,ocr,handwriting,recognition,math,handwriting,free" />
content="公式识别,数学公式,OCR,手写公式识别,印刷体识别,AI识别,数学工具,免费,formula recognition,math formula,handwriting recognition,latex,mathml,markdown,texpixel,TexPixel,混合文字识别,document recognition" />
<meta name="author" content="TexPixel Team" />
<meta name="robots" content="index, follow" />
<!-- Open Graph Meta Tags -->
<meta property="og:title" content="TexPixel - 公式识别工具" />
<meta property="og:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
<!-- Open Graph Meta Tags - Bilingual -->
<meta property="og:title" content="TexPixel - 公式识别工具 | Formula Recognition Tool" />
<meta property="og:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。Online formula recognition tool supporting printed and handwritten math formulas." />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://texpixel.com/" />
<meta property="og:image" content="https://cdn.texpixel.com/public/logo.png?x-oss-process=image/resize,w_600" />
<meta property="og:locale" content="zh_CN" />
<meta property="og:locale:alternate" content="en_US" />
<meta property="og:site_name" content="TexPixel" />
<!-- Twitter Card Meta Tags -->
<!-- Twitter Card Meta Tags - Bilingual -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="TexPixel - 公式识别工具" />
<meta name="twitter:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。" />
<meta name="twitter:title" content="TexPixel - Formula Recognition Tool | 公式识别工具" />
<meta name="twitter:description" content="Online formula recognition tool supporting printed and handwritten math formulas. 支持印刷体和手写体数学公式识别。" />
<meta name="twitter:image" content="https://cdn.texpixel.com/public/logo.png?x-oss-process=image/resize,w_600" />
<meta name="twitter:site" content="@TexPixel" />
<!-- baidu -->
<!-- Baidu Verification -->
<meta name="baidu-site-verification" content="codeva-8zU93DeGgH" />
<!-- Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "TexPixel",
"url": "https://texpixel.com/",
"inLanguage": ["zh-CN", "en"],
"description": "Formula recognition tool for converting images and PDFs into LaTeX, MathML, and Markdown."
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "TexPixel",
"applicationCategory": "BusinessApplication",
"operatingSystem": "Web",
"url": "https://texpixel.com/",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"description": "Online OCR and formula recognition for printed and handwritten mathematical expressions."
}
</script>
<!-- Language Detection Script -->
<script>
// Update HTML lang attribute based on user preference or browser language
(function() {
const savedLang = localStorage.getItem('language');
const browserLang = navigator.language.toLowerCase();
const isZh = savedLang === 'zh' || (!savedLang && browserLang.startsWith('zh'));
document.documentElement.lang = isZh ? 'zh-CN' : 'en';
// Update page title based on language
if (!isZh) {
document.title = '⚡️ TexPixel - Formula Recognition Tool';
}
})();
</script>
</head>
<body>
@@ -37,4 +91,4 @@
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>

27
package-lock.json generated
View File

@@ -18,6 +18,7 @@
"mathml2omml": "^0.5.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.6.0",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",
@@ -2804,6 +2805,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/goober": {
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
"license": "MIT",
"peerDependencies": {
"csstype": "^3.0.10"
}
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -4575,6 +4585,23 @@
"react": "^18.3.1"
}
},
"node_modules/react-hot-toast": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",
"integrity": "sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==",
"license": "MIT",
"dependencies": {
"csstype": "^3.1.3",
"goober": "^2.1.16"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16",
"react-dom": ">=16"
}
},
"node_modules/react-markdown": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",

View File

@@ -23,6 +23,7 @@
"mathml2omml": "^0.5.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hot-toast": "^2.6.0",
"react-markdown": "^10.1.0",
"rehype-katex": "^7.0.1",
"remark-breaks": "^4.0.0",

167
public/en/index.html Normal file
View File

@@ -0,0 +1,167 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TexPixel - Formula Recognition Tool for LaTeX, MathML, and Markdown</title>
<meta name="description" content="TexPixel converts printed and handwritten math formulas from images and PDFs into LaTeX, MathML, and Markdown." />
<meta name="robots" content="index, follow" />
<meta name="author" content="TexPixel Team" />
<link rel="canonical" href="https://texpixel.com/en/" />
<link rel="alternate" hreflang="zh-CN" href="https://texpixel.com/" />
<link rel="alternate" hreflang="en" href="https://texpixel.com/en/" />
<link rel="alternate" hreflang="x-default" href="https://texpixel.com/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="TexPixel" />
<meta property="og:locale" content="en_US" />
<meta property="og:locale:alternate" content="zh_CN" />
<meta property="og:url" content="https://texpixel.com/en/" />
<meta property="og:title" content="TexPixel - Formula Recognition Tool" />
<meta property="og:description" content="Extract formulas from images and PDFs to editable LaTeX, MathML, and Markdown." />
<meta property="og:image" content="https://cdn.texpixel.com/public/logo.png?x-oss-process=image/resize,w_600" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site" content="@TexPixel" />
<meta name="twitter:title" content="TexPixel - Formula Recognition Tool" />
<meta name="twitter:description" content="Convert mathematical content from images and PDFs into LaTeX, MathML, and Markdown." />
<meta name="twitter:image" content="https://cdn.texpixel.com/public/logo.png?x-oss-process=image/resize,w_600" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebPage",
"name": "TexPixel English",
"url": "https://texpixel.com/en/",
"inLanguage": "en",
"description": "Formula recognition landing page in English."
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What can TexPixel recognize?",
"acceptedAnswer": {
"@type": "Answer",
"text": "TexPixel recognizes printed and handwritten mathematical formulas from images and PDF pages."
}
},
{
"@type": "Question",
"name": "Which output formats are supported?",
"acceptedAnswer": {
"@type": "Answer",
"text": "TexPixel supports LaTeX, MathML, and Markdown outputs for downstream editing and publishing."
}
},
{
"@type": "Question",
"name": "Do I need to install software?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No installation is required. TexPixel works as a web application."
}
}
]
}
</script>
<style>
:root {
--bg: #f7f8fc;
--text: #111827;
--muted: #4b5563;
--accent: #0f766e;
--accent-hover: #0d5f59;
--card: #ffffff;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
color: var(--text);
background: radial-gradient(circle at 20% 20%, #eefbf8 0%, var(--bg) 40%, #f6f8ff 100%);
line-height: 1.6;
}
.wrap {
max-width: 980px;
margin: 0 auto;
padding: 48px 20px 64px;
}
.hero, .section {
background: var(--card);
border-radius: 14px;
padding: 28px;
box-shadow: 0 10px 30px rgba(17, 24, 39, 0.06);
margin-bottom: 20px;
}
h1 {
margin: 0 0 8px;
font-size: clamp(28px, 4vw, 42px);
line-height: 1.2;
}
h2 {
margin: 0 0 12px;
font-size: 24px;
}
p { margin: 0 0 14px; color: var(--muted); }
ul { margin: 0; padding-left: 20px; color: var(--muted); }
li { margin-bottom: 8px; }
.cta {
display: inline-block;
margin-top: 10px;
background: var(--accent);
color: #fff;
text-decoration: none;
font-weight: 600;
padding: 12px 18px;
border-radius: 10px;
}
.cta:hover { background: var(--accent-hover); }
.small { font-size: 14px; }
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<h1>Formula Recognition for Real Math Workflows</h1>
<p>TexPixel converts formulas from screenshots, photos, and PDF pages into editable text formats for researchers, students, and engineering teams.</p>
<a class="cta" id="open-app" href="/">Open TexPixel App</a>
<p class="small">The app opens at the main product URL and defaults to English for this entry point.</p>
</section>
<section class="section">
<h2>Core Capabilities</h2>
<ul>
<li>Recognize printed and handwritten formulas from image or PDF input.</li>
<li>Export to LaTeX for papers, MathML for web workflows, and Markdown for docs.</li>
<li>Use the browser-based workflow without local software installation.</li>
</ul>
</section>
<section class="section">
<h2>FAQ</h2>
<p><strong>Is TexPixel browser-based?</strong><br />Yes. You can upload files and get output directly in the web app.</p>
<p><strong>What content type works best?</strong><br />Clean scans and high-contrast screenshots improve recognition quality.</p>
<p><strong>Can I reuse output in technical documents?</strong><br />Yes. LaTeX and Markdown outputs are intended for editing and reuse.</p>
</section>
</main>
<script>
(function () {
var openApp = document.getElementById('open-app');
if (!openApp) return;
openApp.addEventListener('click', function () {
try {
localStorage.setItem('language', 'en');
} catch (err) {
// Keep navigation working even if storage is unavailable.
}
});
})();
</script>
</body>
</html>

14
public/llms.txt Normal file
View File

@@ -0,0 +1,14 @@
# TexPixel
TexPixel is a web tool for converting images and PDFs into editable mathematical formats such as LaTeX, MathML, and Markdown.
Canonical URL: https://texpixel.com/
Primary languages: zh-CN, en
## Preferred page for citation
- https://texpixel.com/
- https://texpixel.com/en/
## Crawl guidance
- Public product content is allowed for indexing.
- Avoid API and authenticated/private areas.

7
public/robots.txt Normal file
View File

@@ -0,0 +1,7 @@
User-agent: *
Allow: /
Disallow: /api/
Disallow: /auth/
Disallow: /admin/
Sitemap: https://texpixel.com/sitemap.xml

15
public/sitemap.xml Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://texpixel.com/</loc>
<lastmod>2026-02-25</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://texpixel.com/en/</loc>
<lastmod>2026-02-25</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
</urlset>

View File

@@ -180,7 +180,7 @@ function App() {
markdown_content: item.markdown,
latex_content: item.latex,
mathml_content: item.mathml,
mathml_word_content: item.mathml_mw,
mml: item.mml,
rendered_image_path: item.image_blob || null,
created_at: item.created_at,
};
@@ -287,7 +287,7 @@ function App() {
markdown_content: result.markdown,
latex_content: result.latex,
mathml_content: result.mathml,
mathml_word_content: result.mathml_mw,
mml: result.mml,
rendered_image_path: result.image_blob || null,
created_at: new Date().toISOString(),
};
@@ -344,7 +344,7 @@ function App() {
markdown_content: result.markdown,
latex_content: result.latex,
mathml_content: result.mathml,
mathml_word_content: result.mathml_mw,
mml: result.mml,
rendered_image_path: result.image_blob || null,
created_at: new Date().toISOString()
};

View File

@@ -1,11 +1,12 @@
import { useState } from 'react';
import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, Loader2, LucideIcon } from 'lucide-react';
import { RecognitionResult } from '../types';
import { convertMathmlToOmml, wrapOmmlForClipboard } from '../lib/ommlConverter';
import { generateImageFromElement, copyImageToClipboard, downloadImage } from '../lib/imageGenerator';
import { API_BASE_URL } from '../config/env';
import { tokenManager } from '../lib/api';
import { trackExportEvent } from '../lib/analytics';
import { useLanguage } from '../contexts/LanguageContext';
import toast, { Toaster } from 'react-hot-toast';
interface ExportSidebarProps {
isOpen: boolean;
@@ -39,14 +40,25 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
category: 'Code',
getContent: (r) => r.markdown_content
},
{
id: 'latex',
label: 'LaTeX',
category: 'Code',
getContent: (r) => r.latex_content
},
{
id: 'latex_inline',
label: 'LaTeX (Inline)',
category: 'Code',
getContent: (r) => {
if (!r.latex_content) return null;
// Remove existing \[ \] and wrap with \( \)
const content = r.latex_content.replace(/^\\\[/, '').replace(/\\\]$/, '').trim();
// Remove existing delimiters like \[ \], \( \), $$, or $
let content = r.latex_content.trim();
content = content.replace(/^\\\[/, '').replace(/\\\]$/, '');
content = content.replace(/^\\\(/, '').replace(/\\\)$/, '');
content = content.replace(/^\$\$/, '').replace(/\$\$$/, '');
content = content.replace(/^\$/, '').replace(/\$$/, '');
content = content.trim();
return `\\(${content}\\)`;
}
},
@@ -54,7 +66,17 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
id: 'latex_display',
label: 'LaTeX (Display)',
category: 'Code',
getContent: (r) => r.latex_content
getContent: (r) => {
if (!r.latex_content) return null;
// Remove existing delimiters like \[ \], \( \), $$, or $
let content = r.latex_content.trim();
content = content.replace(/^\\\[/, '').replace(/\\\]$/, '');
content = content.replace(/^\\\(/, '').replace(/\\\)$/, '');
content = content.replace(/^\$\$/, '').replace(/\$\$$/, '');
content = content.replace(/^\$/, '').replace(/\$$/, '');
content = content.trim();
return `\\[${content}\\]`;
}
},
{
id: 'mathml',
@@ -63,10 +85,10 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
getContent: (r) => r.mathml_content
},
{
id: 'mathml_word',
label: 'Word MathML',
id: 'mathml_mml',
label: 'MathML (MML)',
category: 'Code',
getContent: (r) => r.mathml_word_content
getContent: (r) => r.mml
},
// Image Category
{
@@ -169,6 +191,15 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
};
const handleAction = async (option: ExportOption) => {
// Analytics tracking
if (result?.id) {
trackExportEvent(
result.id,
option.id,
exportOptions.map(o => o.id)
);
}
// Handle DOCX export via API
if (option.id === 'docx') {
await handleFileExport('docx');
@@ -181,22 +212,33 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
return;
}
let content = option.getContent(result);
const content = option.getContent(result);
// Fallback: If Word MathML is missing, try to convert from MathML
if (option.id === 'mathml_word' && !content && result.mathml_content) {
try {
const omml = await convertMathmlToOmml(result.mathml_content);
if (omml) {
content = wrapOmmlForClipboard(omml);
}
} catch (err) {
console.error('Failed to convert MathML to OMML:', err);
}
// Check if content is empty and show toast
if (!content) {
toast.error(t.export.noContent, {
duration: 2000,
position: 'top-center',
style: {
background: '#fff',
color: '#1f2937',
padding: '16px 20px',
borderRadius: '12px',
boxShadow: '0 10px 40px rgba(59, 130, 246, 0.15), 0 2px 8px rgba(59, 130, 246, 0.1)',
border: '1px solid #dbeafe',
fontSize: '14px',
fontWeight: '500',
maxWidth: '900px',
lineHeight: '1.5',
},
iconTheme: {
primary: '#ef4444',
secondary: '#ffffff',
},
});
return;
}
if (!content) return;
setExportingId(option.id);
try {
@@ -224,6 +266,10 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
} catch (err) {
console.error('Action failed:', err);
toast.error(t.export.failed, {
duration: 3000,
position: 'top-center',
});
} finally {
setExportingId(null);
}
@@ -237,6 +283,41 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
return (
<>
{/* Toast Container with custom configuration */}
<Toaster
position="top-center"
toastOptions={{
duration: 3000,
style: {
background: '#fff',
color: '#1f2937',
fontSize: '14px',
fontWeight: '500',
borderRadius: '12px',
boxShadow: '0 10px 40px rgba(59, 130, 246, 0.15), 0 2px 8px rgba(59, 130, 246, 0.1)',
maxWidth: '420px',
},
success: {
iconTheme: {
primary: '#10b981',
secondary: '#ffffff',
},
style: {
border: '1px solid #d1fae5',
},
},
error: {
iconTheme: {
primary: '#3b82f6',
secondary: '#ffffff',
},
style: {
border: '1px solid #dbeafe',
},
},
}}
/>
{/* Backdrop */}
{isOpen && (
<div

View File

@@ -1,6 +1,7 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { translations, Language, TranslationKey } from '../lib/translations';
import { detectLanguageByIP } from '../lib/ipLocation';
import { updatePageMeta } from '../lib/seoHelper';
interface LanguageContextType {
language: Language;
@@ -25,6 +26,7 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil
// 如果用户已经手动选择过语言则不进行IP检测
if (saved === 'en' || saved === 'zh') {
updatePageMeta(saved); // Update meta tags on initial load
return;
}
@@ -32,18 +34,21 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil
detectLanguageByIP()
.then((detectedLang) => {
setLanguageState(detectedLang);
updatePageMeta(detectedLang); // Update meta tags after detection
// 注意:这里不保存到 localStorage让用户首次访问时使用IP检测的结果
// 如果用户手动切换语言,才会保存到 localStorage
})
.catch((error) => {
// IP检测失败时保持使用浏览器语言检测的结果
console.warn('Failed to detect language by IP:', error);
updatePageMeta(language); // Update with fallback language
});
}, []); // 仅在组件挂载时执行一次
const setLanguage = (lang: Language) => {
setLanguageState(lang);
localStorage.setItem('language', lang);
updatePageMeta(lang); // Update page metadata when language changes
};
const t = translations[language];

63
src/lib/analytics.ts Normal file
View File

@@ -0,0 +1,63 @@
import http from './api';
interface AnalyticsPayload {
task_no: string;
event_name: string;
properties: Record<string, any>;
meta_data?: Record<string, any>;
device_info: {
ip: string;
"use-agent": string;
browser: string;
};
}
export const trackExportEvent = (
taskNo: string,
selectedOption: string,
availableOptions: string[]
) => {
try {
const payload: AnalyticsPayload = {
task_no: taskNo,
event_name: 'export_selected_event',
properties: {
option: availableOptions,
selected: selectedOption
},
meta_data: {
task_no: taskNo
},
device_info: {
ip: '',
"use-agent": navigator.userAgent,
browser: getBrowserName()
}
};
// Fire and forget - do not await
http.post('/analytics/track', payload).catch(err => {
// Silently ignore errors to not block business flow
console.debug('Analytics tracking failed:', err);
});
} catch (error) {
console.debug('Analytics error:', error);
}
};
function getBrowserName(): string {
const userAgent = navigator.userAgent;
if (userAgent.match(/chrome|chromium|crios/i)) {
return "Chrome";
} else if (userAgent.match(/firefox|fxios/i)) {
return "Firefox";
} else if (userAgent.match(/safari/i)) {
return "Safari";
} else if (userAgent.match(/opr\//i)) {
return "Opera";
} else if (userAgent.match(/edg/i)) {
return "Edge";
} else {
return "Unknown";
}
}

View File

@@ -63,54 +63,6 @@ function renderLatexToHtml(latex: string, fontSize: number): string {
}
}
/**
* Renders Markdown with LaTeX to HTML
* For complex Markdown, we extract and render math blocks separately
*/
function renderMarkdownToHtml(markdown: string, fontSize: number): string {
// Simple approach: extract LaTeX blocks and render them
// For full Markdown support, you'd use a Markdown parser
let html = markdown;
// Replace display math $$ ... $$
html = html.replace(/\$\$([\s\S]*?)\$\$/g, (_, latex) => {
try {
return `<div class="math-block">${katex.renderToString(latex.trim(), {
throwOnError: false,
displayMode: true,
output: 'html',
strict: false,
})}</div>`;
} catch {
return `<div class="math-block" style="color: red;">Error: ${latex}</div>`;
}
});
// Replace inline math $ ... $
html = html.replace(/\$([^$\n]+)\$/g, (_, latex) => {
try {
return katex.renderToString(latex.trim(), {
throwOnError: false,
displayMode: false,
output: 'html',
strict: false,
});
} catch {
return `<span style="color: red;">Error: ${latex}</span>`;
}
});
// Basic Markdown: newlines to <br>
html = html.replace(/\n/g, '<br>');
return `
<div style="font-size: ${fontSize}px; line-height: 1.8; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
${html}
</div>
`;
}
/**
* Waits for all KaTeX fonts to be loaded
*/

View File

@@ -93,16 +93,35 @@ Where:
</mfrac>
</mrow>
</math>`,
mathml_word_content: `<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">
<m:f>
<m:num>
<m:r><m:t>-b ± √(b²-4ac)</m:t></m:r>
</m:num>
<m:den>
<m:r><m:t>2a</m:t></m:r>
</m:den>
</m:f>
</m:oMath>`,
mml: `<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block">
<mml:mrow>
<mml:mi>x</mml:mi>
<mml:mo>=</mml:mo>
<mml:mfrac>
<mml:mrow>
<mml:mo>-</mml:mo>
<mml:mi>b</mml:mi>
<mml:mo>±</mml:mo>
<mml:msqrt>
<mml:mrow>
<mml:msup>
<mml:mi>b</mml:mi>
<mml:mn>2</mml:mn>
</mml:msup>
<mml:mo>-</mml:mo>
<mml:mn>4</mml:mn>
<mml:mi>a</mml:mi>
<mml:mi>c</mml:mi>
</mml:mrow>
</mml:msqrt>
</mml:mrow>
<mml:mrow>
<mml:mn>2</mml:mn>
<mml:mi>a</mml:mi>
</mml:mrow>
</mml:mfrac>
</mml:mrow>
</mml:math>`,
rendered_image_path: 'https://images.pexels.com/photos/3729557/pexels-photo-3729557.jpeg?auto=compress&cs=tinysrgb&w=800',
created_at: new Date().toISOString(),
};
@@ -161,7 +180,7 @@ Where:
markdown_content: `# Analysis for ${filename}\n\nThis is a mock analysis result generated for the uploaded file.\n\n$$ E = mc^2 $$\n\nDetected content matches widely known physics formulas.`,
latex_content: `\\documentclass{article}\n\\begin{document}\nSection{${filename}}\n\n\\[ E = mc^2 \\]\n\n\\end{document}`,
mathml_content: `<math><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></math>`,
mathml_word_content: `<m:oMath><m:r><m:t>E=mc^2</m:t></m:r></m:oMath>`,
mml: `<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML"><mml:mi>E</mml:mi><mml:mo>=</mml:mo><mml:mi>m</mml:mi><mml:msup><mml:mi>c</mml:mi><mml:mn>2</mml:mn></mml:msup></mml:math>`,
rendered_image_path: 'https://images.pexels.com/photos/3729557/pexels-photo-3729557.jpeg?auto=compress&cs=tinysrgb&w=800', // Placeholder
created_at: new Date().toISOString(),
};

80
src/lib/seoHelper.ts Normal file
View File

@@ -0,0 +1,80 @@
import { Language } from './translations';
interface SEOContent {
title: string;
description: string;
keywords: string;
}
const seoContent: Record<Language, SEOContent> = {
zh: {
title: '⚡️ TexPixel - 公式识别工具',
description: '在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。',
keywords: '公式识别,数学公式,OCR,手写公式识别,印刷体识别,AI识别,数学工具,免费,混合文字识别,texpixel,TexPixel',
},
en: {
title: '⚡️ TexPixel - Formula Recognition Tool',
description: 'Online formula recognition tool supporting printed and handwritten math formulas. Convert images to LaTeX, MathML, and Markdown quickly and accurately.',
keywords: 'formula recognition,math formula,OCR,handwriting recognition,latex,mathml,markdown,AI recognition,math tool,free,texpixel,TexPixel,document recognition',
},
};
/**
* Update document metadata based on current language
*/
export function updatePageMeta(language: Language): void {
const content = seoContent[language];
// Update title
document.title = content.title;
// Update HTML lang attribute
document.documentElement.lang = language === 'zh' ? 'zh-CN' : 'en';
// Update meta description
const metaDescription = document.querySelector('meta[name="description"]');
if (metaDescription) {
metaDescription.setAttribute('content', content.description);
}
// Update meta keywords
const metaKeywords = document.querySelector('meta[name="keywords"]');
if (metaKeywords) {
metaKeywords.setAttribute('content', content.keywords);
}
// Update Open Graph meta tags
const ogTitle = document.querySelector('meta[property="og:title"]');
if (ogTitle) {
ogTitle.setAttribute('content', content.title);
}
const ogDescription = document.querySelector('meta[property="og:description"]');
if (ogDescription) {
ogDescription.setAttribute('content', content.description);
}
// Update Twitter Card meta tags
const twitterTitle = document.querySelector('meta[name="twitter:title"]');
if (twitterTitle) {
twitterTitle.setAttribute('content', content.title);
}
const twitterDescription = document.querySelector('meta[name="twitter:description"]');
if (twitterDescription) {
twitterDescription.setAttribute('content', content.description);
}
// Update og:locale
const ogLocale = document.querySelector('meta[property="og:locale"]');
if (ogLocale) {
ogLocale.setAttribute('content', language === 'zh' ? 'zh_CN' : 'en_US');
}
}
/**
* Get SEO content for a specific language
*/
export function getSEOContent(language: Language): SEOContent {
return seoContent[language];
}

View File

@@ -61,7 +61,7 @@ export type Database = {
markdown_content: string | null;
latex_content: string | null;
mathml_content: string | null;
mathml_word_content: string | null;
mml: string | null;
rendered_image_path: string | null;
created_at: string;
};
@@ -71,7 +71,7 @@ export type Database = {
markdown_content?: string | null;
latex_content?: string | null;
mathml_content?: string | null;
mathml_word_content?: string | null;
mml?: string | null;
rendered_image_path?: string | null;
created_at?: string;
};
@@ -81,7 +81,7 @@ export type Database = {
markdown_content?: string | null;
latex_content?: string | null;
mathml_content?: string | null;
mathml_word_content?: string | null;
mml?: string | null;
rendered_image_path?: string | null;
created_at?: string;
};

View File

@@ -70,6 +70,8 @@ export const translations = {
},
failed: 'Export failed, please try again',
imageFailed: 'Failed to generate image',
noContent: 'Mixed text and formulas do not support LaTeX/MathML export. Please download DOCX format instead.',
noContentShort: 'Not supported for mixed content',
},
guide: {
next: 'Next',
@@ -164,6 +166,8 @@ export const translations = {
},
failed: '导出失败,请重试',
imageFailed: '生成图片失败',
noContent: '混合文字内容不支持 LaTeX/MathML 导出,请下载 DOCX 文件。',
noContentShort: '混合内容不支持',
},
guide: {
next: '下一步',

View File

@@ -5,12 +5,37 @@ import './index.css';
import { AuthProvider } from './contexts/AuthContext';
import { LanguageProvider } from './contexts/LanguageContext';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<AuthProvider>
<LanguageProvider>
<App />
</LanguageProvider>
</AuthProvider>
</StrictMode>
);
// 错误处理:捕获未处理的错误
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
});
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
});
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Root element not found');
}
try {
createRoot(rootElement).render(
<StrictMode>
<AuthProvider>
<LanguageProvider>
<App />
</LanguageProvider>
</AuthProvider>
</StrictMode>
);
} catch (error) {
console.error('Failed to render app:', error);
rootElement.innerHTML = `
<div style="padding: 20px; font-family: sans-serif;">
<h1>应用启动失败</h1>
<p>错误信息: ${error instanceof Error ? error.message : String(error)}</p>
<p>请检查浏览器控制台获取更多信息。</p>
</div>
`;
}

View File

@@ -79,7 +79,7 @@ export interface RecognitionResultData {
latex: string;
markdown: string;
mathml: string;
mathml_mw: string; // MathML for Word
mml: string; // MathML with mml: prefix
image_blob: string; // Base64 or URL? assuming string content
docx_url: string;
pdf_url: string;
@@ -96,7 +96,7 @@ export interface TaskHistoryItem {
latex: string;
markdown: string;
mathml: string;
mathml_mw: string;
mml: string;
image_blob: string;
docx_url: string;
pdf_url: string;

View File

@@ -17,7 +17,7 @@ export interface RecognitionResult {
markdown_content: string | null;
latex_content: string | null;
mathml_content: string | null;
mathml_word_content: string | null;
mml: string | null;
rendered_image_path: string | null;
created_at: string;
}
@@ -26,7 +26,6 @@ export type ExportFormat =
| 'markdown'
| 'latex'
| 'mathml'
| 'mathml-word'
| 'image'
| 'docx'
| 'pdf'