feat: add toast for no content
This commit is contained in:
194
app.cloud/assets/index-D6FBTwaJ.js
Normal file
194
app.cloud/assets/index-D6FBTwaJ.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,36 +1,63 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>⚡️ TexPixel - 公式识别工具</title>
|
|
||||||
|
<!-- Multi-language Support -->
|
||||||
|
<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 -->
|
<!-- SEO Meta Tags - Chinese (Default) -->
|
||||||
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
|
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。Online formula recognition tool supporting printed and handwritten math formulas." />
|
||||||
<meta name="keywords"
|
<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="author" content="TexPixel Team" />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags - Bilingual -->
|
||||||
<meta property="og:title" content="TexPixel - 公式识别工具" />
|
<meta property="og:title" content="TexPixel - 公式识别工具 | Formula Recognition Tool" />
|
||||||
<meta property="og:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
|
<meta property="og:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。Online formula recognition tool supporting printed and handwritten math formulas." />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://texpixel.com/" />
|
<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: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:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="TexPixel - 公式识别工具" />
|
<meta name="twitter:title" content="TexPixel - Formula Recognition Tool | 公式识别工具" />
|
||||||
<meta name="twitter:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。" />
|
<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: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" />
|
<meta name="baidu-site-verification" content="codeva-8zU93DeGgH" />
|
||||||
|
|
||||||
<script type="module" crossorigin src="/assets/index-NjWMZQkP.js"></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>
|
||||||
|
|
||||||
|
<script type="module" crossorigin src="/assets/index-D6FBTwaJ.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-react-C6WG4Va-.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-C6WG4Va-.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-katex-p018AHG0.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-katex-p018AHG0.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-markdown-C0b4qDwm.js">
|
<link rel="modulepreload" crossorigin href="/assets/vendor-markdown-C0b4qDwm.js">
|
||||||
|
|||||||
140
docs/MULTILANG_IMPLEMENTATION.md
Normal file
140
docs/MULTILANG_IMPLEMENTATION.md
Normal 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
174
docs/MULTILANG_README.md
Normal 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)
|
||||||
51
index.html
51
index.html
@@ -1,35 +1,62 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>⚡️ TexPixel - 公式识别工具</title>
|
|
||||||
|
<!-- Multi-language Support -->
|
||||||
|
<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 -->
|
<!-- SEO Meta Tags - Chinese (Default) -->
|
||||||
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
|
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。Online formula recognition tool supporting printed and handwritten math formulas." />
|
||||||
<meta name="keywords"
|
<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="author" content="TexPixel Team" />
|
||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
|
|
||||||
<!-- Open Graph Meta Tags -->
|
<!-- Open Graph Meta Tags - Bilingual -->
|
||||||
<meta property="og:title" content="TexPixel - 公式识别工具" />
|
<meta property="og:title" content="TexPixel - 公式识别工具 | Formula Recognition Tool" />
|
||||||
<meta property="og:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
|
<meta property="og:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。Online formula recognition tool supporting printed and handwritten math formulas." />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://texpixel.com/" />
|
<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: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:card" content="summary_large_image" />
|
||||||
<meta name="twitter:title" content="TexPixel - 公式识别工具" />
|
<meta name="twitter:title" content="TexPixel - Formula Recognition Tool | 公式识别工具" />
|
||||||
<meta name="twitter:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。" />
|
<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: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" />
|
<meta name="baidu-site-verification" content="codeva-8zU93DeGgH" />
|
||||||
|
|
||||||
|
<!-- 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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
27
package-lock.json
generated
27
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"mathml2omml": "^0.5.0",
|
"mathml2omml": "^0.5.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
@@ -2804,6 +2805,15 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/graphemer": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
|
||||||
@@ -4575,6 +4585,23 @@
|
|||||||
"react": "^18.3.1"
|
"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": {
|
"node_modules/react-markdown": {
|
||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
"mathml2omml": "^0.5.0",
|
"mathml2omml": "^0.5.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-hot-toast": "^2.6.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-breaks": "^4.0.0",
|
"remark-breaks": "^4.0.0",
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ function App() {
|
|||||||
markdown_content: item.markdown,
|
markdown_content: item.markdown,
|
||||||
latex_content: item.latex,
|
latex_content: item.latex,
|
||||||
mathml_content: item.mathml,
|
mathml_content: item.mathml,
|
||||||
mathml_word_content: item.mathml_mw,
|
mml: item.mml,
|
||||||
rendered_image_path: item.image_blob || null,
|
rendered_image_path: item.image_blob || null,
|
||||||
created_at: item.created_at,
|
created_at: item.created_at,
|
||||||
};
|
};
|
||||||
@@ -287,7 +287,7 @@ function App() {
|
|||||||
markdown_content: result.markdown,
|
markdown_content: result.markdown,
|
||||||
latex_content: result.latex,
|
latex_content: result.latex,
|
||||||
mathml_content: result.mathml,
|
mathml_content: result.mathml,
|
||||||
mathml_word_content: result.mathml_mw,
|
mml: result.mml,
|
||||||
rendered_image_path: result.image_blob || null,
|
rendered_image_path: result.image_blob || null,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -344,7 +344,7 @@ function App() {
|
|||||||
markdown_content: result.markdown,
|
markdown_content: result.markdown,
|
||||||
latex_content: result.latex,
|
latex_content: result.latex,
|
||||||
mathml_content: result.mathml,
|
mathml_content: result.mathml,
|
||||||
mathml_word_content: result.mathml_mw,
|
mml: result.mml,
|
||||||
rendered_image_path: result.image_blob || null,
|
rendered_image_path: result.image_blob || null,
|
||||||
created_at: new Date().toISOString()
|
created_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, Loader2, LucideIcon } from 'lucide-react';
|
import { X, Check, Copy, Download, Code2, Image as ImageIcon, FileText, Loader2, LucideIcon } from 'lucide-react';
|
||||||
import { RecognitionResult } from '../types';
|
import { RecognitionResult } from '../types';
|
||||||
import { convertMathmlToOmml, wrapOmmlForClipboard } from '../lib/ommlConverter';
|
|
||||||
import { generateImageFromElement, copyImageToClipboard, downloadImage } from '../lib/imageGenerator';
|
import { generateImageFromElement, copyImageToClipboard, downloadImage } from '../lib/imageGenerator';
|
||||||
import { API_BASE_URL } from '../config/env';
|
import { API_BASE_URL } from '../config/env';
|
||||||
import { tokenManager } from '../lib/api';
|
import { tokenManager } from '../lib/api';
|
||||||
import { trackExportEvent } from '../lib/analytics';
|
import { trackExportEvent } from '../lib/analytics';
|
||||||
import { useLanguage } from '../contexts/LanguageContext';
|
import { useLanguage } from '../contexts/LanguageContext';
|
||||||
|
import toast, { Toaster } from 'react-hot-toast';
|
||||||
|
|
||||||
interface ExportSidebarProps {
|
interface ExportSidebarProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -25,87 +25,6 @@ interface ExportOption {
|
|||||||
extension?: string;
|
extension?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to add mml: prefix to MathML
|
|
||||||
const addMMLPrefix = (mathml: string): string | null => {
|
|
||||||
try {
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const xmlDoc = parser.parseFromString(mathml, 'application/xml');
|
|
||||||
|
|
||||||
// Check for parse errors
|
|
||||||
const parseError = xmlDoc.getElementsByTagName("parsererror");
|
|
||||||
if (parseError.length > 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new document with mml namespace
|
|
||||||
const newDoc = document.implementation.createDocument(
|
|
||||||
'http://www.w3.org/1998/Math/MathML',
|
|
||||||
'mml:math',
|
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const newMathElement = newDoc.documentElement;
|
|
||||||
|
|
||||||
// Copy display attribute if present
|
|
||||||
const displayAttr = xmlDoc.documentElement.getAttribute('display');
|
|
||||||
if (displayAttr) {
|
|
||||||
newMathElement.setAttribute('display', displayAttr);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursive function to process nodes
|
|
||||||
const processNode = (node: Node, newParent: Element) => {
|
|
||||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
||||||
const element = node as Element;
|
|
||||||
// Create new element with mml: prefix in the target document
|
|
||||||
const newElement = newDoc.createElementNS(
|
|
||||||
'http://www.w3.org/1998/Math/MathML',
|
|
||||||
'mml:' + element.localName
|
|
||||||
);
|
|
||||||
|
|
||||||
// Copy attributes
|
|
||||||
for (let i = 0; i < element.attributes.length; i++) {
|
|
||||||
const attr = element.attributes[i];
|
|
||||||
// Skip xmlns attributes as we handle them explicitly
|
|
||||||
if (attr.name.startsWith('xmlns')) continue;
|
|
||||||
|
|
||||||
newElement.setAttributeNS(
|
|
||||||
attr.namespaceURI,
|
|
||||||
attr.name,
|
|
||||||
attr.value
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process children
|
|
||||||
Array.from(element.childNodes).forEach(child => {
|
|
||||||
processNode(child, newElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
newParent.appendChild(newElement);
|
|
||||||
} else if (node.nodeType === Node.TEXT_NODE) {
|
|
||||||
newParent.appendChild(newDoc.createTextNode(node.nodeValue || ''));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Process all children of the root math element
|
|
||||||
Array.from(xmlDoc.documentElement.childNodes).forEach(child => {
|
|
||||||
processNode(child, newMathElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Serialize
|
|
||||||
const serializer = new XMLSerializer();
|
|
||||||
let prefixedMathML = serializer.serializeToString(newDoc);
|
|
||||||
|
|
||||||
// Clean up xmlns
|
|
||||||
prefixedMathML = prefixedMathML.replace(/ xmlns(:mml)?="[^"]*"/g, '');
|
|
||||||
prefixedMathML = prefixedMathML.replace(/<mml:math>/, '<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML">');
|
|
||||||
|
|
||||||
return prefixedMathML;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to process MathML:', err);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebarProps) {
|
export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebarProps) {
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const [copiedId, setCopiedId] = useState<string | null>(null);
|
const [copiedId, setCopiedId] = useState<string | null>(null);
|
||||||
@@ -121,14 +40,25 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
|||||||
category: 'Code',
|
category: 'Code',
|
||||||
getContent: (r) => r.markdown_content
|
getContent: (r) => r.markdown_content
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'latex',
|
||||||
|
label: 'LaTeX',
|
||||||
|
category: 'Code',
|
||||||
|
getContent: (r) => r.latex_content
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'latex_inline',
|
id: 'latex_inline',
|
||||||
label: 'LaTeX (Inline)',
|
label: 'LaTeX (Inline)',
|
||||||
category: 'Code',
|
category: 'Code',
|
||||||
getContent: (r) => {
|
getContent: (r) => {
|
||||||
if (!r.latex_content) return null;
|
if (!r.latex_content) return null;
|
||||||
// Remove existing \[ \] and wrap with \( \)
|
// Remove existing delimiters like \[ \], \( \), $$, or $
|
||||||
const content = r.latex_content.replace(/^\\\[/, '').replace(/\\\]$/, '').trim();
|
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}\\)`;
|
return `\\(${content}\\)`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -136,7 +66,17 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
|||||||
id: 'latex_display',
|
id: 'latex_display',
|
||||||
label: 'LaTeX (Display)',
|
label: 'LaTeX (Display)',
|
||||||
category: 'Code',
|
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',
|
id: 'mathml',
|
||||||
@@ -148,13 +88,7 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
|||||||
id: 'mathml_mml',
|
id: 'mathml_mml',
|
||||||
label: 'MathML (MML)',
|
label: 'MathML (MML)',
|
||||||
category: 'Code',
|
category: 'Code',
|
||||||
getContent: (r) => r.mathml_content ? addMMLPrefix(r.mathml_content) : null
|
getContent: (r) => r.mml
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mathml_word',
|
|
||||||
label: 'Word MathML',
|
|
||||||
category: 'Code',
|
|
||||||
getContent: (r) => r.mathml_word_content
|
|
||||||
},
|
},
|
||||||
// Image Category
|
// Image Category
|
||||||
{
|
{
|
||||||
@@ -278,22 +212,33 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = option.getContent(result);
|
const content = option.getContent(result);
|
||||||
|
|
||||||
// Fallback: If Word MathML is missing, try to convert from MathML
|
// Check if content is empty and show toast
|
||||||
if (option.id === 'mathml_word' && !content && result.mathml_content) {
|
if (!content) {
|
||||||
try {
|
toast.error(t.export.noContent, {
|
||||||
const omml = await convertMathmlToOmml(result.mathml_content);
|
duration: 2000,
|
||||||
if (omml) {
|
position: 'top-center',
|
||||||
content = wrapOmmlForClipboard(omml);
|
style: {
|
||||||
}
|
background: '#fff',
|
||||||
} catch (err) {
|
color: '#1f2937',
|
||||||
console.error('Failed to convert MathML to OMML:', err);
|
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);
|
setExportingId(option.id);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -321,6 +266,10 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Action failed:', err);
|
console.error('Action failed:', err);
|
||||||
|
toast.error(t.export.failed, {
|
||||||
|
duration: 3000,
|
||||||
|
position: 'top-center',
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setExportingId(null);
|
setExportingId(null);
|
||||||
}
|
}
|
||||||
@@ -334,6 +283,41 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
|
|||||||
|
|
||||||
return (
|
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 */}
|
{/* Backdrop */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { translations, Language, TranslationKey } from '../lib/translations';
|
import { translations, Language, TranslationKey } from '../lib/translations';
|
||||||
import { detectLanguageByIP } from '../lib/ipLocation';
|
import { detectLanguageByIP } from '../lib/ipLocation';
|
||||||
|
import { updatePageMeta } from '../lib/seoHelper';
|
||||||
|
|
||||||
interface LanguageContextType {
|
interface LanguageContextType {
|
||||||
language: Language;
|
language: Language;
|
||||||
@@ -25,6 +26,7 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
|
|
||||||
// 如果用户已经手动选择过语言,则不进行IP检测
|
// 如果用户已经手动选择过语言,则不进行IP检测
|
||||||
if (saved === 'en' || saved === 'zh') {
|
if (saved === 'en' || saved === 'zh') {
|
||||||
|
updatePageMeta(saved); // Update meta tags on initial load
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,18 +34,21 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil
|
|||||||
detectLanguageByIP()
|
detectLanguageByIP()
|
||||||
.then((detectedLang) => {
|
.then((detectedLang) => {
|
||||||
setLanguageState(detectedLang);
|
setLanguageState(detectedLang);
|
||||||
|
updatePageMeta(detectedLang); // Update meta tags after detection
|
||||||
// 注意:这里不保存到 localStorage,让用户首次访问时使用IP检测的结果
|
// 注意:这里不保存到 localStorage,让用户首次访问时使用IP检测的结果
|
||||||
// 如果用户手动切换语言,才会保存到 localStorage
|
// 如果用户手动切换语言,才会保存到 localStorage
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
// IP检测失败时,保持使用浏览器语言检测的结果
|
// IP检测失败时,保持使用浏览器语言检测的结果
|
||||||
console.warn('Failed to detect language by IP:', error);
|
console.warn('Failed to detect language by IP:', error);
|
||||||
|
updatePageMeta(language); // Update with fallback language
|
||||||
});
|
});
|
||||||
}, []); // 仅在组件挂载时执行一次
|
}, []); // 仅在组件挂载时执行一次
|
||||||
|
|
||||||
const setLanguage = (lang: Language) => {
|
const setLanguage = (lang: Language) => {
|
||||||
setLanguageState(lang);
|
setLanguageState(lang);
|
||||||
localStorage.setItem('language', lang);
|
localStorage.setItem('language', lang);
|
||||||
|
updatePageMeta(lang); // Update page metadata when language changes
|
||||||
};
|
};
|
||||||
|
|
||||||
const t = translations[language];
|
const t = translations[language];
|
||||||
|
|||||||
@@ -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
|
* Waits for all KaTeX fonts to be loaded
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -93,16 +93,35 @@ Where:
|
|||||||
</mfrac>
|
</mfrac>
|
||||||
</mrow>
|
</mrow>
|
||||||
</math>`,
|
</math>`,
|
||||||
mathml_word_content: `<m:oMath xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math">
|
mml: `<mml:math xmlns:mml="http://www.w3.org/1998/Math/MathML" display="block">
|
||||||
<m:f>
|
<mml:mrow>
|
||||||
<m:num>
|
<mml:mi>x</mml:mi>
|
||||||
<m:r><m:t>-b ± √(b²-4ac)</m:t></m:r>
|
<mml:mo>=</mml:mo>
|
||||||
</m:num>
|
<mml:mfrac>
|
||||||
<m:den>
|
<mml:mrow>
|
||||||
<m:r><m:t>2a</m:t></m:r>
|
<mml:mo>-</mml:mo>
|
||||||
</m:den>
|
<mml:mi>b</mml:mi>
|
||||||
</m:f>
|
<mml:mo>±</mml:mo>
|
||||||
</m:oMath>`,
|
<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',
|
rendered_image_path: 'https://images.pexels.com/photos/3729557/pexels-photo-3729557.jpeg?auto=compress&cs=tinysrgb&w=800',
|
||||||
created_at: new Date().toISOString(),
|
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.`,
|
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}`,
|
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_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
|
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(),
|
created_at: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|||||||
80
src/lib/seoHelper.ts
Normal file
80
src/lib/seoHelper.ts
Normal 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];
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ export type Database = {
|
|||||||
markdown_content: string | null;
|
markdown_content: string | null;
|
||||||
latex_content: string | null;
|
latex_content: string | null;
|
||||||
mathml_content: string | null;
|
mathml_content: string | null;
|
||||||
mathml_word_content: string | null;
|
mml: string | null;
|
||||||
rendered_image_path: string | null;
|
rendered_image_path: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
};
|
};
|
||||||
@@ -71,7 +71,7 @@ export type Database = {
|
|||||||
markdown_content?: string | null;
|
markdown_content?: string | null;
|
||||||
latex_content?: string | null;
|
latex_content?: string | null;
|
||||||
mathml_content?: string | null;
|
mathml_content?: string | null;
|
||||||
mathml_word_content?: string | null;
|
mml?: string | null;
|
||||||
rendered_image_path?: string | null;
|
rendered_image_path?: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
};
|
};
|
||||||
@@ -81,7 +81,7 @@ export type Database = {
|
|||||||
markdown_content?: string | null;
|
markdown_content?: string | null;
|
||||||
latex_content?: string | null;
|
latex_content?: string | null;
|
||||||
mathml_content?: string | null;
|
mathml_content?: string | null;
|
||||||
mathml_word_content?: string | null;
|
mml?: string | null;
|
||||||
rendered_image_path?: string | null;
|
rendered_image_path?: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
failed: 'Export failed, please try again',
|
failed: 'Export failed, please try again',
|
||||||
imageFailed: 'Failed to generate image',
|
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: {
|
guide: {
|
||||||
next: 'Next',
|
next: 'Next',
|
||||||
@@ -164,6 +166,8 @@ export const translations = {
|
|||||||
},
|
},
|
||||||
failed: '导出失败,请重试',
|
failed: '导出失败,请重试',
|
||||||
imageFailed: '生成图片失败',
|
imageFailed: '生成图片失败',
|
||||||
|
noContent: '混合文字内容不支持 LaTeX/MathML 导出,请下载 DOCX 文件。',
|
||||||
|
noContentShort: '混合内容不支持',
|
||||||
},
|
},
|
||||||
guide: {
|
guide: {
|
||||||
next: '下一步',
|
next: '下一步',
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export interface RecognitionResultData {
|
|||||||
latex: string;
|
latex: string;
|
||||||
markdown: string;
|
markdown: string;
|
||||||
mathml: string;
|
mathml: string;
|
||||||
mathml_mw: string; // MathML for Word
|
mml: string; // MathML with mml: prefix
|
||||||
image_blob: string; // Base64 or URL? assuming string content
|
image_blob: string; // Base64 or URL? assuming string content
|
||||||
docx_url: string;
|
docx_url: string;
|
||||||
pdf_url: string;
|
pdf_url: string;
|
||||||
@@ -96,7 +96,7 @@ export interface TaskHistoryItem {
|
|||||||
latex: string;
|
latex: string;
|
||||||
markdown: string;
|
markdown: string;
|
||||||
mathml: string;
|
mathml: string;
|
||||||
mathml_mw: string;
|
mml: string;
|
||||||
image_blob: string;
|
image_blob: string;
|
||||||
docx_url: string;
|
docx_url: string;
|
||||||
pdf_url: string;
|
pdf_url: string;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface RecognitionResult {
|
|||||||
markdown_content: string | null;
|
markdown_content: string | null;
|
||||||
latex_content: string | null;
|
latex_content: string | null;
|
||||||
mathml_content: string | null;
|
mathml_content: string | null;
|
||||||
mathml_word_content: string | null;
|
mml: string | null;
|
||||||
rendered_image_path: string | null;
|
rendered_image_path: string | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,6 @@ export type ExportFormat =
|
|||||||
| 'markdown'
|
| 'markdown'
|
||||||
| 'latex'
|
| 'latex'
|
||||||
| 'mathml'
|
| 'mathml'
|
||||||
| 'mathml-word'
|
|
||||||
| 'image'
|
| 'image'
|
||||||
| 'docx'
|
| 'docx'
|
||||||
| 'pdf'
|
| 'pdf'
|
||||||
|
|||||||
Reference in New Issue
Block a user