feat: add track point
This commit is contained in:
@@ -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
63
src/lib/analytics.ts
Normal 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";
|
||||
}
|
||||
}
|
||||
43
src/main.tsx
43
src/main.tsx
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user