Spaces:
Running
Running
| /** | |
| * Reasoning Trace Parser | |
| * Handles parsing and formatting of model reasoning traces from OCR outputs | |
| */ | |
| class ReasoningParser { | |
| /** | |
| * Detect if text contains reasoning trace markers | |
| * @param {string} text - The text to check | |
| * @returns {boolean} - True if reasoning trace is detected | |
| */ | |
| static detectReasoningTrace(text) { | |
| if (!text || typeof text !== 'string') return false; | |
| // Check for complete reasoning trace patterns (both opening and closing tags) | |
| const completePatterns = [ | |
| { start: /<think>/i, end: /<\/think>/i }, | |
| { start: /<thinking>/i, end: /<\/thinking>/i }, | |
| { start: /<reasoning>/i, end: /<\/reasoning>/i }, | |
| { start: /<thought>/i, end: /<\/thought>/i } | |
| ]; | |
| // Only return true if we find BOTH opening and closing tags | |
| return completePatterns.some(pattern => | |
| pattern.start.test(text) && pattern.end.test(text) | |
| ); | |
| } | |
| /** | |
| * Parse reasoning content from text | |
| * @param {string} text - The text containing reasoning trace | |
| * @returns {object} - Object with reasoning and answer sections | |
| */ | |
| static parseReasoningContent(text) { | |
| if (!text) { | |
| return { reasoning: null, answer: null, original: text }; | |
| } | |
| // Try multiple patterns for flexibility | |
| const patterns = [ | |
| { | |
| start: /<think>/i, | |
| end: /<\/think>/i, | |
| answerStart: /<answer>/i, | |
| answerEnd: /<\/answer>/i | |
| }, | |
| { | |
| start: /<thinking>/i, | |
| end: /<\/thinking>/i, | |
| answerStart: /<answer>/i, | |
| answerEnd: /<\/answer>/i | |
| }, | |
| { | |
| start: /<reasoning>/i, | |
| end: /<\/reasoning>/i, | |
| answerStart: /<output>/i, | |
| answerEnd: /<\/output>/i | |
| } | |
| ]; | |
| for (const pattern of patterns) { | |
| const reasoningMatch = text.match(new RegExp( | |
| pattern.start.source + '([\\s\\S]*?)' + pattern.end.source, | |
| 'i' | |
| )); | |
| const answerMatch = text.match(new RegExp( | |
| pattern.answerStart.source + '([\\s\\S]*?)' + pattern.answerEnd.source, | |
| 'i' | |
| )); | |
| if (reasoningMatch || answerMatch) { | |
| return { | |
| reasoning: reasoningMatch ? reasoningMatch[1].trim() : null, | |
| answer: answerMatch ? answerMatch[1].trim() : null, | |
| hasReasoning: !!reasoningMatch, | |
| hasAnswer: !!answerMatch, | |
| original: text | |
| }; | |
| } | |
| } | |
| // Check if there are incomplete reasoning tags (opening but no closing) | |
| const hasOpeningTag = /<think>|<thinking>|<reasoning>|<thought>/i.test(text); | |
| if (hasOpeningTag) { | |
| console.warn('Incomplete reasoning trace detected - missing closing tags'); | |
| } | |
| // If no patterns match, return original text as answer | |
| return { | |
| reasoning: null, | |
| answer: text, | |
| hasReasoning: false, | |
| hasAnswer: true, | |
| original: text | |
| }; | |
| } | |
| /** | |
| * Format reasoning steps for display | |
| * @param {string} reasoningText - The raw reasoning text | |
| * @returns {object} - Formatted reasoning with steps and metadata | |
| */ | |
| static formatReasoningSteps(reasoningText) { | |
| if (!reasoningText) return null; | |
| // Parse numbered steps (e.g., "1. Step content") | |
| const stepPattern = /^\d+\.\s+\*\*(.+?)\*\*(.+?)(?=^\d+\.\s|\z)/gms; | |
| const steps = []; | |
| let match; | |
| while ((match = stepPattern.exec(reasoningText)) !== null) { | |
| steps.push({ | |
| title: match[1].trim(), | |
| content: match[2].trim() | |
| }); | |
| } | |
| // If no numbered steps found, try to parse by line breaks | |
| if (steps.length === 0) { | |
| const lines = reasoningText.split('\n').filter(line => line.trim()); | |
| lines.forEach((line, index) => { | |
| // Check if line starts with a number | |
| const numberedMatch = line.match(/^(\d+)\.\s*(.+)/); | |
| if (numberedMatch) { | |
| const title = numberedMatch[2].replace(/\*\*/g, '').trim(); | |
| steps.push({ | |
| number: numberedMatch[1], | |
| title: title, | |
| content: '' | |
| }); | |
| } else if (steps.length > 0) { | |
| // Add to previous step's content | |
| steps[steps.length - 1].content += '\n' + line; | |
| } | |
| }); | |
| } | |
| return { | |
| steps: steps, | |
| rawText: reasoningText, | |
| stepCount: steps.length, | |
| characterCount: reasoningText.length, | |
| wordCount: reasoningText.split(/\s+/).filter(w => w).length | |
| }; | |
| } | |
| /** | |
| * Extract key insights from reasoning | |
| * @param {string} reasoningText - The reasoning text | |
| * @returns {array} - Array of key insights or decisions | |
| */ | |
| static extractInsights(reasoningText) { | |
| if (!reasoningText) return []; | |
| const insights = []; | |
| // Look for decision points and key observations | |
| const patterns = [ | |
| /decision:\s*(.+)/gi, | |
| /observation:\s*(.+)/gi, | |
| /note:\s*(.+)/gi, | |
| /important:\s*(.+)/gi, | |
| /key finding:\s*(.+)/gi | |
| ]; | |
| patterns.forEach(pattern => { | |
| let match; | |
| while ((match = pattern.exec(reasoningText)) !== null) { | |
| insights.push(match[1].trim()); | |
| } | |
| }); | |
| return insights; | |
| } | |
| /** | |
| * Get summary statistics about the reasoning trace | |
| * @param {object} parsedContent - Parsed reasoning content | |
| * @returns {object} - Statistics about the reasoning | |
| */ | |
| static getReasoningStats(parsedContent) { | |
| if (!parsedContent || !parsedContent.reasoning) { | |
| return { | |
| hasReasoning: false, | |
| reasoningLength: 0, | |
| answerLength: 0, | |
| reasoningRatio: 0 | |
| }; | |
| } | |
| const reasoningLength = parsedContent.reasoning.length; | |
| const answerLength = parsedContent.answer ? parsedContent.answer.length : 0; | |
| const totalLength = reasoningLength + answerLength; | |
| return { | |
| hasReasoning: true, | |
| reasoningLength: reasoningLength, | |
| answerLength: answerLength, | |
| totalLength: totalLength, | |
| reasoningRatio: totalLength > 0 ? (reasoningLength / totalLength * 100).toFixed(1) : 0, | |
| reasoningWords: parsedContent.reasoning.split(/\s+/).filter(w => w).length, | |
| answerWords: parsedContent.answer ? parsedContent.answer.split(/\s+/).filter(w => w).length : 0 | |
| }; | |
| } | |
| /** | |
| * Format reasoning for export | |
| * @param {object} parsedContent - Parsed reasoning content | |
| * @param {boolean} includeReasoning - Whether to include reasoning in export | |
| * @returns {string} - Formatted text for export | |
| */ | |
| static formatForExport(parsedContent, includeReasoning = true) { | |
| if (!parsedContent) return ''; | |
| let exportText = ''; | |
| if (includeReasoning && parsedContent.reasoning) { | |
| exportText += '=== MODEL REASONING ===\n\n'; | |
| exportText += parsedContent.reasoning; | |
| exportText += '\n\n=== FINAL OUTPUT ===\n\n'; | |
| } | |
| if (parsedContent.answer) { | |
| exportText += parsedContent.answer; | |
| } | |
| return exportText; | |
| } | |
| } | |
| // Export for use in other scripts | |
| window.ReasoningParser = ReasoningParser; |