feat: add track point

This commit is contained in:
liuyuanchuang
2026-01-27 23:44:18 +08:00
parent 7c5409a6c7
commit d562d67203
71 changed files with 957 additions and 11 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@

44
app.cloud/index.html Normal file
View File

@@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<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>
<!-- SEO Meta Tags -->
<meta name="description" content="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
<meta name="keywords"
content="公式识别,数学公式,OCR,手写公式识别,印刷体识别,AI识别,数学工具,免费,handwriting,formula,recognition,ocr,handwriting,recognition,math,handwriting,free" />
<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="在线公式识别工具,支持印刷体和手写体数学公式识别,快速准确地将图片中的数学公式转换为可编辑文本。" />
<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" />
<!-- Twitter Card Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="TexPixel - 公式识别工具" />
<meta name="twitter:description" content="在线公式识别工具,支持印刷体和手写体数学公式识别。" />
<meta name="twitter:image" content="https://cdn.texpixel.com/public/logo.png?x-oss-process=image/resize,w_600" />
<!-- baidu -->
<meta name="baidu-site-verification" content="codeva-8zU93DeGgH" />
<script type="module" crossorigin src="/assets/index-NjWMZQkP.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">
<link rel="stylesheet" crossorigin href="/assets/index-DKZ56yfB.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

203
deploy_dev.sh Executable file
View File

@@ -0,0 +1,203 @@
#!/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"
# Sudo 密码(如果需要,建议配置无密码 sudo 更安全)
# 配置无密码 sudo: 在服务器上运行: echo "username ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/username
SUDO_PASSWORD="1231"
# 打印带颜色的消息
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_SUDO_PASSWORD="${SUDO_PASSWORD}" ssh ${server} bash << SSH_EOF
set -e
DEPLOY_PATH="${DEPLOY_PATH}"
DEPLOY_NAME="${DEPLOY_NAME}"
SUDO_PASSWORD="\${SSH_SUDO_PASSWORD}"
# 检查部署目录是否存在
if [ ! -d "\${DEPLOY_PATH}" ]; then
echo "错误:部署目录 \${DEPLOY_PATH} 不存在,请检查路径是否正确"
exit 1
fi
# 检查是否有权限写入(尝试创建测试文件)
if ! touch "\${DEPLOY_PATH}/.deploy_test" 2>/dev/null; then
echo "提示:没有直接写入权限,将使用 sudo 执行操作"
USE_SUDO=1
else
rm -f "\${DEPLOY_PATH}/.deploy_test"
USE_SUDO=0
fi
# 备份旧版本(如果存在)
if [ -d "\${DEPLOY_PATH}/\${DEPLOY_NAME}" ]; then
echo "备份旧版本..."
if [ "\$USE_SUDO" = "1" ]; then
echo "\${SUDO_PASSWORD}" | sudo -S rm -rf "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" 2>/dev/null || true
echo "\${SUDO_PASSWORD}" | sudo -S mv "\${DEPLOY_PATH}/\${DEPLOY_NAME}" "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" || { echo "错误:备份失败,权限不足"; exit 1; }
else
rm -rf "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" 2>/dev/null || true
mv "\${DEPLOY_PATH}/\${DEPLOY_NAME}" "\${DEPLOY_PATH}/\${DEPLOY_NAME}_bak" || { echo "错误:备份失败"; exit 1; }
fi
fi
# 移动新版本到部署目录(覆盖现有目录)
if [ -d ~/\${DEPLOY_NAME} ]; then
echo "移动新版本到部署目录..."
if [ "\$USE_SUDO" = "1" ]; then
echo "\${SUDO_PASSWORD}" | sudo -S mv ~/\${DEPLOY_NAME} "\${DEPLOY_PATH}/" || { echo "错误:移动文件失败,权限不足"; exit 1; }
else
mv ~/\${DEPLOY_NAME} "\${DEPLOY_PATH}/" || { echo "错误:移动文件失败"; exit 1; }
fi
echo "部署完成!"
else
echo "错误:找不到 ~/\${DEPLOY_NAME} 目录"
exit 1
fi
# 重新加载 nginx如果配置了
if command -v nginx &> /dev/null; then
echo "重新加载 nginx..."
echo "\${SUDO_PASSWORD}" | sudo -S nginx -t && echo "\${SUDO_PASSWORD}" | sudo -S nginx -s reload || echo "警告nginx 重新加载失败,请手动检查"
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

@@ -5,6 +5,7 @@ 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';
interface ExportSidebarProps {
@@ -24,6 +25,87 @@ 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);
@@ -62,6 +144,12 @@ export default function ExportSidebar({ isOpen, onClose, result }: ExportSidebar
category: 'Code',
getContent: (r) => r.mathml_content
},
{
id: 'mathml_mml',
label: 'MathML (MML)',
category: 'Code',
getContent: (r) => r.mathml_content ? addMMLPrefix(r.mathml_content) : null
},
{
id: 'mathml_word',
label: 'Word MathML',
@@ -169,6 +257,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');

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

@@ -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>
`;
}