feat: add toast for no content

This commit is contained in:
liuyuanchuang
2026-02-05 18:22:30 +08:00
parent d562d67203
commit 2b1da79bbc
18 changed files with 832 additions and 390 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +1,63 @@
<!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>
<!-- 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 -->
<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" />
<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-katex-p018AHG0.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-markdown-C0b4qDwm.js">

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,62 @@
<!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>
<!-- 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 -->
<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" />
<!-- 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>

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",

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,12 +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;
@@ -25,87 +25,6 @@ interface ExportOption {
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) {
const { t } = useLanguage();
const [copiedId, setCopiedId] = useState<string | null>(null);
@@ -121,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}\\)`;
}
},
@@ -136,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',
@@ -148,13 +88,7 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
id: 'mathml_mml',
label: 'MathML (MML)',
category: 'Code',
getContent: (r) => r.mathml_content ? addMMLPrefix(r.mathml_content) : null
},
{
id: 'mathml_word',
label: 'Word MathML',
category: 'Code',
getContent: (r) => r.mathml_word_content
getContent: (r) => r.mml
},
// Image Category
{
@@ -278,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 {
@@ -321,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);
}
@@ -334,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];

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

@@ -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'