Spaces:
Sleeping
Sleeping
| import { API_URL, API_TOKEN, API_USER } from '../constants'; | |
| import { ApiInput, ApiResponseOutput, ProcessedResult, QaSectionResult, DetailedQaReport } from '../types'; | |
| const MAX_RETRIES = 3; | |
| const INITIAL_RETRY_DELAY_MS = 1000; | |
| const RETRYABLE_STATUS_CODES = [429, 502, 503, 504]; // 429: Too Many Requests, 5xx: Server Errors | |
| // Add jitter to delay to prevent thundering herd problem | |
| const delay = (ms: number) => new Promise(res => setTimeout(res, ms + Math.random() * 500)); | |
| // Structured error for workflow failures | |
| class WorkflowError extends Error { | |
| code: string; | |
| at: 'network' | 'api' | 'stream' | 'parse' | 'unknown'; | |
| debug?: string; | |
| constructor(code: string, message: string, at: 'network' | 'api' | 'stream' | 'parse' | 'unknown' = 'unknown', debug?: string) { | |
| super(message); | |
| this.name = 'WorkflowError'; | |
| this.code = code; | |
| this.at = at; | |
| this.debug = debug; | |
| } | |
| } | |
| /** | |
| * Removes <think>...</think> blocks from a string. | |
| */ | |
| const cleanResponseText = (text: string): string => { | |
| if (typeof text !== 'string') return ''; | |
| return text.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); | |
| }; | |
| /** | |
| * Parses a single section of the QA report (e.g., TITLE, H1). | |
| * This uses regular expressions to be robust against multiline content and format variations. | |
| * @param sectionText The text content of a single QA section. | |
| * @returns A structured object with the section's results. | |
| */ | |
| const parseSection = (sectionText: string): QaSectionResult => { | |
| console.log('Parsing section text:', sectionText.substring(0, 200)); | |
| // ROBUST GRADE EXTRACTION - handles multiple formats | |
| let grade = 'N/A'; | |
| let gradeMatch = null; | |
| // Try various grade patterns in order of specificity | |
| const gradePatterns = [ | |
| /-\s*\*\*Grade:\*\*\s*(.*)/, // - **Grade:** 100/100 | |
| /•\s*\*\*Grade:\*\*\s*(.*)/, // • **Grade:** 100/100 | |
| /\*\s*\*\*Grade:\*\*\s*(.*)/, // * **Grade:** 100/100 | |
| /-\s*\*\*Grade\*\*:\s*(.*)/, // - **Grade**: 100/100 (colon without space) | |
| /•\s*\*\*Grade\*\*:\s*(.*)/, // • **Grade**: 100/100 (colon without space) | |
| /\*\s*\*\*Grade\*\*:\s*(.*)/, // * **Grade**: 100/100 | |
| /(?:•|-|\*)\s*\*\*Grade\*\*:?:\s*(.*)/, // •/**/- **Grade**: 100/100 or - **Grade** 100/100 | |
| /(?:•|-|\*)\s*Grade:?:\s*(.*)/, // • Grade: 100/100 or - Grade 100/100 | |
| /Grade:?:\s*(\d+\/\d+|\d+)/m, // Grade: 100/100 (anywhere in text) | |
| /(\d+\/\d+)\s*(?:grade|Grade)/ // 100/100 grade (reverse order) | |
| ]; | |
| for (const pattern of gradePatterns) { | |
| gradeMatch = sectionText.match(pattern); | |
| if (gradeMatch) { | |
| grade = gradeMatch[1].trim(); | |
| break; | |
| } | |
| } | |
| console.log('Grade match result:', gradeMatch, 'Final grade:', grade); | |
| // ROBUST PASS EXTRACTION - handles multiple formats including the actual QA Guard format | |
| let pass = false; | |
| let passMatch = null; | |
| // Try various pass patterns in order of specificity - FIXED TO HANDLE ACTUAL QA GUARD FORMAT | |
| const passPatterns = [ | |
| // First check for the actual QA Guard format: "### **TITLE: PASS** ✅" | |
| /###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i, | |
| /###\s*\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i, | |
| /\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*\s*(✅|❌)/i, | |
| /\*\*[^:]+:\s*(PASS|FAIL)\s*\*\*/i, | |
| // Then check for traditional patterns | |
| /-\s*\*\*Pass:\*\*\s*(.*)/i, // - **Pass:** true | |
| /•\s*\*\*Pass:\*\*\s*(.*)/i, // • **Pass:** true | |
| /\*\s*\*\*Pass:\*\*\s*(.*)/i, // * **Pass:** true | |
| /-\s*\*\*Pass\*\*:\s*(.*)/i, // - **Pass**: true (colon without space) | |
| /•\s*\*\*Pass\*\*:\s*(.*)/i, // • **Pass**: true (colon without space) | |
| /\*\s*\*\*Pass\*\*:\s*(.*)/i, // * **Pass**: true (colon without space) | |
| /(?:•|-|\*)\s*\*\*Pass\*\*:?:\s*(.*)/i, // •/**/- **Pass**: true | |
| /(?:•|-|\*)\s*Pass:?:\s*(.*)/i, // • Pass: true | |
| /Pass:?:\s*(true|false|✅|❌|TRUE|FALSE)/im, // Pass: true (anywhere) | |
| /(true|false|✅|❌|TRUE|FALSE)\s*pass/im // true pass (reverse) | |
| ]; | |
| for (const pattern of passPatterns) { | |
| passMatch = sectionText.match(pattern); | |
| if (passMatch) { | |
| const passValue = passMatch[1].toLowerCase().trim(); | |
| pass = passValue.includes('pass') || | |
| passValue.includes('true') || | |
| passValue.includes('✅') || | |
| passValue === 'yes' || | |
| passValue === 'passed'; | |
| break; | |
| } | |
| } | |
| console.log('Pass match result:', passMatch, 'Final pass:', pass); | |
| // ROBUST ERRORS EXTRACTION - handles multiple formats | |
| let errors: string[] = ['No errors reported.']; | |
| let errorsMatch = null; | |
| // Try various error patterns | |
| const errorPatterns = [ | |
| /-\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Errors:** [] | |
| /•\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Errors:** [] | |
| /\*\s*\*\*Errors:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Errors:** [] | |
| /(?:•|-|\*)\s*\*\*Errors?\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic bullet/dash/star + bold | |
| /(?:•|-|\*)\s*Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic bullet/dash/star + no bold | |
| /Errors:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m // Errors: anywhere in text | |
| ]; | |
| for (const pattern of errorPatterns) { | |
| errorsMatch = sectionText.match(pattern); | |
| if (errorsMatch) break; | |
| } | |
| if (errorsMatch) { | |
| const errorsBlock = errorsMatch[1].trim(); | |
| if (errorsBlock === '[]' || !errorsBlock || errorsBlock.toLowerCase() === 'none') { | |
| errors = ['No errors reported.']; | |
| } else if (errorsBlock.startsWith('[') && errorsBlock.includes(']')) { | |
| // Handle array format: [] | |
| try { | |
| const parsed = JSON.parse(errorsBlock); | |
| errors = Array.isArray(parsed) && parsed.length > 0 ? parsed : ['No errors reported.']; | |
| } catch { | |
| // If JSON parsing fails, treat as plain text | |
| errors = [errorsBlock.replace(/[\[\]]/g, '').trim()]; | |
| } | |
| } else { | |
| // Handle multi-line bullet format or plain text | |
| const lines = errorsBlock.split('\n').map(e => e.trim().replace(/^[-•\*]\s*/, '')).filter(Boolean); | |
| errors = lines.length > 0 ? lines : ['No errors reported.']; | |
| } | |
| } | |
| // ENHANCED LOGIC: If we have a grade of 100/100 and no errors, but pass is still false, | |
| // we should override the pass status based on the grade and errors | |
| if (grade === '100/100' && (!errors || errors.length === 0 || errors[0] === 'No errors reported.')) { | |
| console.log('Overriding pass status: Grade is 100/100 and no errors, setting pass to true'); | |
| pass = true; | |
| } | |
| // Additional logic: If grade is high (80+) and no errors, likely a pass | |
| if (grade !== 'N/A' && grade !== '0/100') { | |
| const gradeNum = parseInt(grade.split('/')[0]); | |
| if (gradeNum >= 80 && (!errors || errors.length === 0 || errors[0] === 'No errors reported.')) { | |
| console.log(`Overriding pass status: Grade is ${grade} (${gradeNum}/100) and no errors, setting pass to true`); | |
| pass = true; | |
| } | |
| } | |
| // ROBUST ANALYSIS/CORRECTED CONTENT EXTRACTION - handles multiple formats | |
| let corrected = 'Content analysis not available.'; | |
| let contentMatch = null; | |
| // Try various content patterns - Analysis, Corrected, or any descriptive text | |
| const contentPatterns = [ | |
| /-\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Analysis:** text | |
| /•\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Analysis:** text | |
| /\*\s*\*\*Analysis:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Analysis:** text | |
| /-\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n-\s*\*\*|$)/, // - **Corrected:** text | |
| /•\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n•\s*\*\*|$)/, // • **Corrected:** text | |
| /\*\s*\*\*Corrected:\*\*\s*([\s\S]*?)(?=\n\*\s*\*\*|$)/, // * **Corrected:** text | |
| /(?:•|-|\*)\s*\*\*(?:Analysis|Corrected)\*\*:?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // Generic | |
| /(?:•|-|\*)\s*(?:Analysis|Corrected):?:\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/, // No bold | |
| /Analysis:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m, // Analysis: anywhere | |
| /Corrected:?:\s*([\s\S]*?)(?=\n(?:•|-|\*|\*\*)|$)/m // Corrected: anywhere | |
| ]; | |
| for (const pattern of contentPatterns) { | |
| contentMatch = sectionText.match(pattern); | |
| if (contentMatch) { | |
| corrected = contentMatch[1].trim(); | |
| break; | |
| } | |
| } | |
| // If no Analysis/Corrected found, extract the section title/content as fallback | |
| if (!contentMatch || corrected.length < 10) { | |
| // Extract title or first meaningful content line | |
| const lines = sectionText.split('\n').map(l => l.trim()).filter(Boolean); | |
| const titleLine = lines.find(line => !line.startsWith('•') && !line.startsWith('-') && !line.startsWith('*') && !line.includes('**') && line.length > 10); | |
| if (titleLine) { | |
| corrected = titleLine; | |
| } | |
| } | |
| // Clean up any extra formatting | |
| corrected = corrected.replace(/^#\s*/, '').replace(/###\s*\*\*[^*]+\*\*/, '').trim(); | |
| console.log('Content match result:', contentMatch, 'Final corrected:', corrected.substring(0, 50)); | |
| console.log('Final section result - Grade:', grade, 'Pass:', pass, 'Errors:', errors); | |
| return { grade, pass, errors, corrected }; | |
| }; | |
| /** | |
| * Parses the structured QA report format that comes as plain text with sections. | |
| * @param qaText The raw structured QA text from the API. | |
| * @returns An object containing the detailed parsed report and top-level pass/grade info. | |
| */ | |
| const parseStructuredQaReport = (qaText: string): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { | |
| const defaultSection: QaSectionResult = { grade: 'N/A', pass: false, errors: ['Parsing failed'], corrected: '' }; | |
| const defaultReport: DetailedQaReport = { | |
| title: { ...defaultSection }, | |
| meta: { ...defaultSection }, | |
| h1: { ...defaultSection }, | |
| copy: { ...defaultSection }, | |
| overall: { grade: 'N/A', pass: false, primaryIssue: 'Parsing failed' } | |
| }; | |
| try { | |
| // Split the text into sections by looking for section headers | |
| const sections = qaText.split(/(?=^## [A-Z]+)/gm).filter(Boolean); | |
| const parsedData: Partial<DetailedQaReport> = {}; | |
| sections.forEach(sectionText => { | |
| const lines = sectionText.trim().split('\n'); | |
| const header = lines[0]?.replace('## ', '').trim().toLowerCase() || ''; | |
| // Extract grade | |
| const gradeLine = lines.find(line => line.includes('- Grade:'))?.trim() || ''; | |
| const gradeMatch = gradeLine.match(/- Grade:\s*(\d+)\/100/) || gradeLine.match(/- Grade:\s*([^\n]+)/); | |
| const grade = gradeMatch ? gradeMatch[1].trim() : 'N/A'; | |
| // Extract pass status | |
| const passLine = lines.find(line => line.includes('- Pass:'))?.trim() || ''; | |
| const passMatch = passLine.match(/- Pass:\s*(true|false)/i); | |
| const pass = passMatch ? passMatch[1].toLowerCase() === 'true' : false; | |
| // Extract errors | |
| let errors: string[] = []; | |
| const errorsLineIndex = lines.findIndex(line => line.includes('- Errors:')); | |
| if (errorsLineIndex !== -1) { | |
| const errorsContent = lines[errorsLineIndex].replace('- Errors:', '').trim(); | |
| if (errorsContent === '[]' || errorsContent === '') { | |
| errors = ['No errors reported.']; | |
| } else { | |
| // Look for multi-line errors | |
| let errorText = errorsContent; | |
| for (let i = errorsLineIndex + 1; i < lines.length; i++) { | |
| if (lines[i].startsWith('- ') && !lines[i].startsWith(' ')) break; | |
| errorText += '\n' + lines[i].trim(); | |
| } | |
| // Parse error list | |
| if (errorText.startsWith('[') && errorText.includes(']')) { | |
| // Handle array format | |
| try { | |
| const parsedErrors = JSON.parse(errorText); | |
| errors = Array.isArray(parsedErrors) ? parsedErrors : [errorText]; | |
| } catch { | |
| errors = [errorText]; | |
| } | |
| } else { | |
| // Handle plain text or bullet list | |
| errors = errorText.split('\n') | |
| .map(e => e.trim().replace(/^- /, '')) | |
| .filter(Boolean); | |
| } | |
| if (errors.length === 0) { | |
| errors = ['No errors reported.']; | |
| } | |
| } | |
| } else { | |
| errors = ['Errors not found.']; | |
| } | |
| // Extract corrected content | |
| let corrected = ''; | |
| const correctedLineIndex = lines.findIndex(line => line.includes('- Corrected:')); | |
| if (correctedLineIndex !== -1) { | |
| corrected = lines.slice(correctedLineIndex) | |
| .join('\n') | |
| .replace('- Corrected:', '') | |
| .trim(); | |
| } else { | |
| corrected = 'Correction not found.'; | |
| } | |
| const sectionResult: QaSectionResult = { grade, pass, errors, corrected }; | |
| if (header.includes('title')) { | |
| parsedData.title = sectionResult; | |
| } else if (header.includes('meta')) { | |
| parsedData.meta = sectionResult; | |
| } else if (header.includes('h1')) { | |
| parsedData.h1 = sectionResult; | |
| } else if (header.includes('copy')) { | |
| parsedData.copy = sectionResult; | |
| } else if (header.includes('overall')) { | |
| // Extract primary issue for overall section | |
| const primaryIssueLine = lines.find(line => line.includes('- Primary Issue:'))?.trim() || ''; | |
| const primaryIssue = primaryIssueLine.replace('- Primary Issue:', '').trim() || 'Not specified.'; | |
| parsedData.overall = { grade, pass, primaryIssue }; | |
| } | |
| }); | |
| const finalReport: DetailedQaReport = { | |
| title: parsedData.title || { ...defaultSection, errors: ['Title section not found'] }, | |
| meta: parsedData.meta || { ...defaultSection, errors: ['Meta section not found'] }, | |
| h1: parsedData.h1 || { ...defaultSection, errors: ['H1 section not found'] }, | |
| copy: parsedData.copy || { ...defaultSection, errors: ['Copy section not found'] }, | |
| overall: parsedData.overall || { grade: 'N/A', pass: false, primaryIssue: 'Overall section not found' } | |
| }; | |
| return { | |
| detailedQaReport: finalReport, | |
| overallPass: finalReport.overall.pass, | |
| overallGrade: finalReport.overall.grade | |
| }; | |
| } catch (error) { | |
| console.error('Error parsing structured QA report:', error); | |
| return { detailedQaReport: defaultReport, overallPass: false, overallGrade: 'N/A' }; | |
| } | |
| }; | |
| /** | |
| * Parses single-section format where all content is in one block. | |
| * @param sectionText The section containing all embedded QA data. | |
| * @param defaultReport Default report structure. | |
| * @returns Parsed QA report data. | |
| */ | |
| const parseSingleSectionFormat = (sectionText: string, defaultReport: DetailedQaReport): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { | |
| console.log('Parsing single-section format'); | |
| // Extract embedded sections by looking for section patterns like "**TITLE:", "**META:", etc. | |
| const titleMatch = sectionText.match(/\*\*TITLE[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
| const metaMatch = sectionText.match(/\*\*META[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
| const h1Match = sectionText.match(/\*\*H1[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
| const copyMatch = sectionText.match(/\*\*COPY[^*]*\*\*([\s\S]*?)(?=\*\*[A-Z]+|$)/i); | |
| const overallMatch = sectionText.match(/\*\*(?:OVERALL|ASSESSMENT)[^*]*\*\*([\s\S]*?)$/i); | |
| const finalReport: DetailedQaReport = { | |
| title: titleMatch ? parseSection(titleMatch[1]) : { ...defaultReport.title, errors: ['Title section not found'] }, | |
| meta: metaMatch ? parseSection(metaMatch[1]) : { ...defaultReport.meta, errors: ['Meta section not found'] }, | |
| h1: h1Match ? parseSection(h1Match[1]) : { ...defaultReport.h1, errors: ['H1 section not found'] }, | |
| copy: copyMatch ? parseSection(copyMatch[1]) : { ...defaultReport.copy, errors: ['Copy section not found'] }, | |
| overall: overallMatch ? { | |
| grade: extractOverallGrade(overallMatch[1]), | |
| pass: extractOverallPass(overallMatch[1]), | |
| primaryIssue: 'Single-section format parsed' | |
| } : { ...defaultReport.overall } | |
| }; | |
| return { | |
| detailedQaReport: finalReport, | |
| overallPass: finalReport.overall.pass, | |
| overallGrade: finalReport.overall.grade | |
| }; | |
| }; | |
| /** | |
| * Helper function to extract overall grade from text. | |
| */ | |
| const extractOverallGrade = (text: string): string => { | |
| const gradeMatch = text.match(/Grade[^:]*:?\s*(\d+(?:\.\d+)?\/?\d*)/i) || text.match(/(\d+(?:\.\d+)?\/\d+)/); | |
| return gradeMatch ? gradeMatch[1].trim() : 'N/A'; | |
| }; | |
| /** | |
| * Helper function to extract overall pass from text. | |
| */ | |
| const extractOverallPass = (text: string): boolean => { | |
| const passMatch = text.match(/Pass[^:]*:?\s*(true|false|✅|❌|TRUE|FALSE)/i); | |
| if (passMatch) { | |
| const passValue = passMatch[1].toLowerCase().trim(); | |
| return passValue.includes('true') || passValue.includes('✅'); | |
| } | |
| return false; | |
| }; | |
| /** | |
| * Enhanced section parsing that captures ALL QA Guard content | |
| */ | |
| const parseEnhancedSection = (sectionBlock: string): QaSectionResult => { | |
| const baseSection = parseSection(sectionBlock); | |
| // Extract detailed assessment content | |
| const detailedAssessmentMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Detailed\s+)?Assessment\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
| const explanationsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Explanation|Reasoning)\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
| // Extract key strengths | |
| const keyStrengthsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*Key\s+Strengths\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
| const strengthsList = keyStrengthsMatch ? | |
| keyStrengthsMatch[1].split('\n') | |
| .map(line => line.replace(/^[-•*]\s*/, '').trim()) | |
| .filter(line => line.length > 0) : undefined; | |
| // Extract recommendations | |
| const recommendationsMatch = sectionBlock.match(/(?:•|-|\*)\s*\*\*(?:Recommendations?|Suggestions?)\*\*?:?\s*([\s\S]*?)(?=\n(?:•|-|\*)\s*\*\*|$)/i); | |
| const recommendationsList = recommendationsMatch ? | |
| recommendationsMatch[1].split('\n') | |
| .map(line => line.replace(/^[-•*]\s*/, '').trim()) | |
| .filter(line => line.length > 0) : undefined; | |
| return { | |
| ...baseSection, | |
| detailedAssessment: detailedAssessmentMatch ? detailedAssessmentMatch[1].trim() : undefined, | |
| explanations: explanationsMatch ? explanationsMatch[1].trim() : undefined, | |
| keyStrengths: strengthsList, | |
| recommendations: recommendationsList, | |
| rawContent: sectionBlock | |
| }; | |
| }; | |
| /** | |
| * Enhanced overall section parsing that captures ALL QA Guard content | |
| */ | |
| const parseEnhancedOverallSection = (sectionBlock: string): { grade: string; pass: boolean; primaryIssue: string; detailedAssessment?: string; keyStrengths?: string[]; recommendations?: string[]; explanations?: string; rawContent?: string } => { | |
| console.log('Parsing enhanced overall section with actual QA Guard format'); | |
| console.log('Overall section preview:', sectionBlock.substring(0, 300)); | |
| let grade = 'N/A'; | |
| let pass = false; | |
| let primaryIssue = 'Overall assessment not available.'; | |
| let detailedAssessment = ''; | |
| let keyStrengths: string[] = []; | |
| let recommendations: string[] = []; | |
| let explanations = ''; | |
| // COMPREHENSIVE OVERALL GRADE PATTERN MATCHING | |
| // Look for the actual QA Guard overall format: "Final Grade: 98.75/100" | |
| const overallGradePatterns = [ | |
| /Final\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Final Grade: 98.75/100 | |
| /Overall\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Grade: 98.75/100 | |
| /Total\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Total Grade: 98.75/100 | |
| /Combined\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Combined Grade: 98.75/100 | |
| /Average\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Average Grade: 98.75/100 | |
| /Mean\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Mean Grade: 98.75/100 | |
| /Composite\s+Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Composite Grade: 98.75/100 | |
| /Final\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Final Score: 98.75/100 | |
| /Overall\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Score: 98.75/100 | |
| /Total\s+Score:\s*(\d+(?:\.\d+)?)\/100/i, // Total Score: 98.75/100 | |
| /Final\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Final Rating: 98.75/100 | |
| /Overall\s+Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Rating: 98.75/100 | |
| /Final\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Final Mark: 98.75/100 | |
| /Overall\s+Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Mark: 98.75/100 | |
| /Final\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, // Final Points: 98.75/100 | |
| /Overall\s+Points:\s*(\d+(?:\.\d+)?)\/100/i, // Overall Points: 98.75/100 | |
| /-?\s*\*\*Final\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Grade:** 98.75/100 | |
| /-?\s*\*\*Overall\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Grade:** 98.75/100 | |
| /-?\s*\*\*Total\s+Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Total Grade:** 98.75/100 | |
| /-?\s*\*\*Final\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Score:** 98.75/100 | |
| /-?\s*\*\*Overall\s+Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Score:** 98.75/100 | |
| /-?\s*\*\*Final\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Final Rating:** 98.75/100 | |
| /-?\s*\*\*Overall\s+Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Overall Rating:** 98.75/100 | |
| /Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Grade: 98.75/100 | |
| /Score:\s*(\d+(?:\.\d+)?)\/100/i, // Score: 98.75/100 | |
| /Rating:\s*(\d+(?:\.\d+)?)\/100/i, // Rating: 98.75/100 | |
| /Mark:\s*(\d+(?:\.\d+)?)\/100/i, // Mark: 98.75/100 | |
| /Points:\s*(\d+(?:\.\d+)?)\/100/i, // Points: 98.75/100 | |
| ]; | |
| let finalGradeMatch = null; | |
| for (const pattern of overallGradePatterns) { | |
| finalGradeMatch = sectionBlock.match(pattern); | |
| if (finalGradeMatch) { | |
| grade = `${finalGradeMatch[1]}/100`; | |
| console.log('Found final grade:', grade); | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE OVERALL PASS STATUS PATTERN MATCHING | |
| // Look for overall pass status: "Overall Pass: FALSE (due to META violation)" | |
| const overallPassPatterns = [ | |
| /Overall\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Pass: FALSE (due to META violation) | |
| /Final\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Pass: FALSE (due to META violation) | |
| /Total\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Total Pass: FALSE (due to META violation) | |
| /Combined\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Combined Pass: FALSE (due to META violation) | |
| /Average\s+Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Average Pass: FALSE (due to META violation) | |
| /Overall\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Status: FALSE (due to META violation) | |
| /Final\s+Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Status: FALSE (due to META violation) | |
| /Overall\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Result: FALSE (due to META violation) | |
| /Final\s+Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Result: FALSE (due to META violation) | |
| /Overall\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Overall Assessment: FALSE (due to META violation) | |
| /Final\s+Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Final Assessment: FALSE (due to META violation) | |
| /-?\s*\*\*Overall\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Overall Pass:** FALSE (due to META violation) | |
| /-?\s*\*\*Final\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Final Pass:** FALSE (due to META violation) | |
| /-?\s*\*\*Total\s+Pass:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Total Pass:** FALSE (due to META violation) | |
| /-?\s*\*\*Overall\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Overall Status:** FALSE (due to META violation) | |
| /-?\s*\*\*Final\s+Status:\*\*\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // - **Final Status:** FALSE (due to META violation) | |
| /Pass:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Pass: FALSE (due to META violation) | |
| /Status:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Status: FALSE (due to META violation) | |
| /Result:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Result: FALSE (due to META violation) | |
| /Assessment:\s*(TRUE|FALSE|PASS|FAIL)(?:\s*\([^)]+\))?/i, // Assessment: FALSE (due to META violation) | |
| ]; | |
| let overallPassMatch = null; | |
| for (const pattern of overallPassPatterns) { | |
| overallPassMatch = sectionBlock.match(pattern); | |
| if (overallPassMatch) { | |
| pass = overallPassMatch[1].toUpperCase() === 'TRUE' || overallPassMatch[1].toUpperCase() === 'PASS'; | |
| console.log('Found overall pass status:', pass); | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE PRIMARY ISSUE PATTERN MATCHING | |
| // Look for primary issue in the pass status explanation | |
| const issuePatterns = [ | |
| /Overall\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Overall Pass: FALSE (due to META violation) | |
| /Final\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Final Pass: FALSE (due to META violation) | |
| /Total\s+Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Total Pass: FALSE (due to META violation) | |
| /Overall\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Overall Status: FALSE (due to META violation) | |
| /Final\s+Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Final Status: FALSE (due to META violation) | |
| /Pass:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Pass: FALSE (due to META violation) | |
| /Status:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Status: FALSE (due to META violation) | |
| /Result:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Result: FALSE (due to META violation) | |
| /Assessment:\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // Assessment: FALSE (due to META violation) | |
| /-?\s*\*\*Overall\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Overall Pass:** FALSE (due to META violation) | |
| /-?\s*\*\*Final\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Final Pass:** FALSE (due to META violation) | |
| /-?\s*\*\*Total\s+Pass:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Total Pass:** FALSE (due to META violation) | |
| /-?\s*\*\*Overall\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Overall Status:** FALSE (due to META violation) | |
| /-?\s*\*\*Final\s+Status:\*\*\s*(?:TRUE|FALSE|PASS|FAIL)\s*\(([^)]+)\)/i, // - **Final Status:** FALSE (due to META violation) | |
| /-?\s*\*\*Primary\s+Issue:\*\*\s*([^\n]+)/i, // - **Primary Issue:** Some sections have violations | |
| /-?\s*\*\*Main\s+Issue:\*\*\s*([^\n]+)/i, // - **Main Issue:** Some sections have violations | |
| /-?\s*\*\*Key\s+Issue:\*\*\s*([^\n]+)/i, // - **Key Issue:** Some sections have violations | |
| /-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, // - **Issue:** Some sections have violations | |
| /-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, // - **Problem:** Some sections have violations | |
| /-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, // - **Concern:** Some sections have violations | |
| /Primary\s+Issue:\s*([^\n]+)/i, // Primary Issue: Some sections have violations | |
| /Main\s+Issue:\s*([^\n]+)/i, // Main Issue: Some sections have violations | |
| /Key\s+Issue:\s*([^\n]+)/i, // Key Issue: Some sections have violations | |
| /Issue:\s*([^\n]+)/i, // Issue: Some sections have violations | |
| /Problem:\s*([^\n]+)/i, // Problem: Some sections have violations | |
| /Concern:\s*([^\n]+)/i, // Concern: Some sections have violations | |
| ]; | |
| let issueMatch = null; | |
| for (const pattern of issuePatterns) { | |
| issueMatch = sectionBlock.match(pattern); | |
| if (issueMatch) { | |
| primaryIssue = issueMatch[1].trim(); | |
| console.log('Found primary issue:', primaryIssue); | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE DETAILED BREAKDOWN PATTERN MATCHING | |
| // Look for detailed breakdown sections | |
| const breakdownPatterns = [ | |
| /##\s+DETAILED\s+BREAKDOWN[^#]*/i, // ## DETAILED BREAKDOWN | |
| /##\s+BREAKDOWN[^#]*/i, // ## BREAKDOWN | |
| /##\s+ANALYSIS[^#]*/i, // ## ANALYSIS | |
| /##\s+ASSESSMENT[^#]*/i, // ## ASSESSMENT | |
| /##\s+EVALUATION[^#]*/i, // ## EVALUATION | |
| /##\s+REVIEW[^#]*/i, // ## REVIEW | |
| /##\s+SUMMARY[^#]*/i, // ## SUMMARY | |
| /##\s+DETAILS[^#]*/i, // ## DETAILS | |
| /##\s+EXPLANATION[^#]*/i, // ## EXPLANATION | |
| /##\s+COMMENTS[^#]*/i, // ## COMMENTS | |
| /##\s+NOTES[^#]*/i, // ## NOTES | |
| /###\s+DETAILED\s+BREAKDOWN[^#]*/i, // ### DETAILED BREAKDOWN | |
| /###\s+BREAKDOWN[^#]*/i, // ### BREAKDOWN | |
| /###\s+ANALYSIS[^#]*/i, // ### ANALYSIS | |
| /###\s+ASSESSMENT[^#]*/i, // ### ASSESSMENT | |
| /###\s+EVALUATION[^#]*/i, // ### EVALUATION | |
| /###\s+REVIEW[^#]*/i, // ### REVIEW | |
| /###\s+SUMMARY[^#]*/i, // ### SUMMARY | |
| /###\s+DETAILS[^#]*/i, // ### DETAILS | |
| /###\s+EXPLANATION[^#]*/i, // ### EXPLANATION | |
| /###\s+COMMENTS[^#]*/i, // ### COMMENTS | |
| /###\s+NOTES[^#]*/i, // ### NOTES | |
| ]; | |
| let breakdownMatch = null; | |
| for (const pattern of breakdownPatterns) { | |
| breakdownMatch = sectionBlock.match(pattern); | |
| if (breakdownMatch) { | |
| detailedAssessment = breakdownMatch[0]; | |
| console.log('Found detailed breakdown'); | |
| break; | |
| } | |
| } | |
| // ENHANCED KEY STRENGTHS AND RECOMMENDATIONS EXTRACTION | |
| // Extract key strengths and recommendations from the overall assessment | |
| const lowerSection = sectionBlock.toLowerCase(); | |
| // Extract key strengths | |
| if (lowerSection.includes('compliance') && lowerSection.includes('requirements')) { | |
| keyStrengths.push('Overall compliance with requirements'); | |
| } | |
| if (grade !== 'N/A' && parseFloat(grade.split('/')[0]) >= 80) { | |
| keyStrengths.push('High overall grade achieved'); | |
| } | |
| if (lowerSection.includes('successful') || lowerSection.includes('approved')) { | |
| keyStrengths.push('Overall assessment successful'); | |
| } | |
| if (lowerSection.includes('meets') && lowerSection.includes('standards')) { | |
| keyStrengths.push('Meets overall standards'); | |
| } | |
| if (lowerSection.includes('satisfies') && lowerSection.includes('criteria')) { | |
| keyStrengths.push('Satisfies overall criteria'); | |
| } | |
| if (lowerSection.includes('valid') || lowerSection.includes('correct')) { | |
| keyStrengths.push('Overall content validation passed'); | |
| } | |
| // Extract recommendations | |
| if (lowerSection.includes('violation') || lowerSection.includes('fail')) { | |
| recommendations.push('Address identified violations'); | |
| } | |
| if (lowerSection.includes('correction') || lowerSection.includes('fix')) { | |
| recommendations.push('Implement suggested corrections'); | |
| } | |
| if (lowerSection.includes('improve') || lowerSection.includes('enhance')) { | |
| recommendations.push('Improve overall content quality'); | |
| } | |
| if (lowerSection.includes('adjust') || lowerSection.includes('modify')) { | |
| recommendations.push('Adjust content to meet requirements'); | |
| } | |
| if (lowerSection.includes('review') && lowerSection.includes('carefully')) { | |
| recommendations.push('Review content carefully'); | |
| } | |
| if (lowerSection.includes('consider') && lowerSection.includes('changes')) { | |
| recommendations.push('Consider suggested changes'); | |
| } | |
| // FALLBACK PATTERN MATCHING | |
| // If no grade found, try alternative patterns | |
| if (grade === 'N/A') { | |
| const fallbackGradePatterns = [ | |
| /Grade:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Score:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Rating:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Mark:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Points:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /(\d+(?:\.\d+)?)\/100/i, | |
| ]; | |
| for (const pattern of fallbackGradePatterns) { | |
| const match = sectionBlock.match(pattern); | |
| if (match) { | |
| grade = `${match[1]}/100`; | |
| console.log('Found fallback grade:', grade); | |
| break; | |
| } | |
| } | |
| } | |
| // INFER PASS STATUS FROM GRADE IF NOT DETERMINED | |
| if (grade !== 'N/A' && !overallPassMatch) { | |
| const gradeNum = parseFloat(grade.split('/')[0]); | |
| pass = gradeNum >= 80; | |
| console.log('Inferred overall pass status from grade:', pass); | |
| } | |
| // FINAL PRIMARY ISSUE DETERMINATION | |
| // If still no primary issue, generate one based on pass status | |
| if (primaryIssue === 'Overall assessment not available.') { | |
| if (pass) { | |
| primaryIssue = 'All sections meet requirements'; | |
| } else { | |
| primaryIssue = 'Some sections have violations'; | |
| } | |
| } | |
| console.log('Final overall result - Grade:', grade, 'Pass:', pass, 'Issue:', primaryIssue); | |
| return { | |
| grade, | |
| pass, | |
| primaryIssue, | |
| detailedAssessment: detailedAssessment || undefined, | |
| keyStrengths: keyStrengths.length > 0 ? keyStrengths : undefined, | |
| recommendations: recommendations.length > 0 ? recommendations : undefined, | |
| explanations: explanations || undefined, | |
| rawContent: sectionBlock | |
| }; | |
| }; | |
| /** | |
| * Determine the type of an additional section based on its content | |
| */ | |
| const determineSectionType = (sectionBlock: string): 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other' => { | |
| const lowerContent = sectionBlock.toLowerCase(); | |
| if (lowerContent.includes('strength') || lowerContent.includes('positive') || lowerContent.includes('excellent')) { | |
| return 'strengths'; | |
| } else if (lowerContent.includes('recommend') || lowerContent.includes('suggest') || lowerContent.includes('improve')) { | |
| return 'recommendations'; | |
| } else if (lowerContent.includes('explain') || lowerContent.includes('reason') || lowerContent.includes('why')) { | |
| return 'explanations'; | |
| } else if (lowerContent.includes('assess') || lowerContent.includes('evaluate') || lowerContent.includes('analysis')) { | |
| return 'assessment'; | |
| } else { | |
| return 'other'; | |
| } | |
| }; | |
| /** | |
| * Parses the new, structured QA report format. | |
| * @param qaText The raw `qa_gaurd` string from the API. | |
| * @returns An object containing the detailed parsed report and top-level pass/grade info. | |
| */ | |
| const parseNewQaReport = (qaText: string): { detailedQaReport: DetailedQaReport, overallPass: boolean, overallGrade: string } => { | |
| // Default structure in case of parsing failure | |
| const defaultSection: QaSectionResult = { grade: 'N/A', pass: false, errors: ['Parsing failed'], corrected: '' }; | |
| const defaultReport: DetailedQaReport = { | |
| title: { ...defaultSection }, | |
| meta: { ...defaultSection }, | |
| h1: { ...defaultSection }, | |
| copy: { ...defaultSection }, | |
| overall: { grade: 'N/A', pass: false, primaryIssue: 'Parsing failed' }, | |
| completeRawReport: qaText // Always preserve the complete raw report | |
| }; | |
| const cleanedQaText = cleanResponseText(qaText); | |
| if (!cleanedQaText || typeof cleanedQaText !== 'string') { | |
| return { detailedQaReport: defaultReport, overallPass: false, overallGrade: 'N/A' }; | |
| } | |
| console.log('Enhanced QA parsing - input text preview:', cleanedQaText.substring(0, 500)); | |
| // COMPLETELY REWRITTEN TO HANDLE ACTUAL QA GUARD FORMAT | |
| // The QA Guard uses formats like: "## **TITLE GRADE: 100/100 ✅ PASS**" | |
| const parsedData: Partial<DetailedQaReport> = {}; | |
| const additionalSections: { [sectionName: string]: { content: string; type: 'assessment' | 'strengths' | 'recommendations' | 'explanations' | 'other'; } } = {}; | |
| // Enhanced section splitting to handle the actual QA Guard format | |
| let sections: string[] = []; | |
| // First, try to remove any leading overall evaluation header that might interfere with section splitting | |
| const contentAfterOverallHeader = cleanedQaText.replace(/^(#\s*FINAL\s+QUALITY\s+ASSURANCE\s+EVALUATION|##\s*Section\s+Grades)\s*/i, '').trim(); | |
| // Try multiple splitting strategies for robustness | |
| // Order matters: more specific patterns should come first | |
| const sectionHeaderPatterns = [ | |
| // Highly specific: ### **SECTION NAME** - GRADE: X/100 ✅ PASS | |
| /(?=(?:##|###)\s*\*\*([^*]+)\*\*\s*-\s*GRADE:)/g, | |
| // Highly specific: ### **SECTION NAME: PASS** ✅ | |
| /(?=(?:##|###)\s*\*\*([^*]+):\s*(?:PASS|FAIL)\*\*\s*(?:✅|❌))/g, | |
| // Specific: ## **SECTION NAME GRADE:** | |
| /(?=(?:##|###)\s*\*\*([^*]+)\s+GRADE:)/g, | |
| // Specific: ## **SECTION NAME** (with optional pass/fail indicator) | |
| /(?=(?:##|###)\s*\*\*([^*]+)\*\*(?:\s*(?:✅|❌|PASS|FAIL))?)/g, | |
| // Generic: ## Section Name | |
| /(?=(?:##|###)\s*[^#\n]*)/g, | |
| ]; | |
| for (const pattern of sectionHeaderPatterns) { | |
| sections = contentAfterOverallHeader.split(pattern).filter(Boolean); | |
| if (sections.length > 1) { // If we found more than one section, this split was successful | |
| console.log(`Using successful split pattern: ${pattern}`); | |
| break; | |
| } | |
| } | |
| // Fallback to paragraph splitting if no suitable header pattern was found | |
| if (sections.length <= 1) { | |
| sections = contentAfterOverallHeader.split(/\n\n+/).filter(section => section.trim().length > 20); | |
| console.log('Using paragraph-based parsing as fallback (no header pattern matched)'); | |
| } | |
| // Ensure sections are not empty after splitting | |
| sections = sections.filter(s => s.trim().length > 5); // Minimum length to be considered a valid section | |
| console.log(`Found ${sections.length} sections to parse`); | |
| sections.forEach((section, index) => { | |
| console.log(`Section ${index} preview:`, section.substring(0, 150)); | |
| }); | |
| // Parse each section with enhanced logic | |
| sections.forEach((sectionBlock, index) => { | |
| const lines = sectionBlock.trim().split('\n'); | |
| const headerRaw = lines[0]?.trim() || ''; | |
| const header = headerRaw.toLowerCase(); | |
| console.log(`Processing section ${index} with header:`, headerRaw); | |
| let sectionType = ''; // Must be reset for each block | |
| let sectionData: QaSectionResult | null = null; | |
| // New, more robust section identification logic | |
| const headerForTypeCheck = header | |
| .replace(/^(#+\s*|\*\*)/, '') | |
| .replace(/[:*]/g, '') | |
| .replace(/\s*-\s*grade:.*/, '') // also strip grade info | |
| .replace(/\s*(✅|❌|pass|fail).*/, '') // and status info | |
| .trim(); | |
| const words = headerForTypeCheck.split(/\s+/).filter(Boolean); | |
| // If the normalized header is just ONE clean word, it's a primary section | |
| if (words.length === 1) { | |
| const typeWord = words[0]; | |
| if (typeWord === 'title') sectionType = 'title'; | |
| else if (typeWord === 'meta') sectionType = 'meta'; | |
| else if (typeWord === 'h1') sectionType = 'h1'; | |
| else if (typeWord === 'copy') sectionType = 'copy'; | |
| else if (['overall', 'final', 'assessment'].includes(typeWord)) sectionType = 'overall'; | |
| } | |
| // If not identified, check for specific multi-word headers for 'overall' | |
| if (!sectionType) { | |
| if (headerForTypeCheck.startsWith('overall assessment') || headerForTypeCheck.startsWith('final assessment')) { | |
| sectionType = 'overall'; | |
| } | |
| } | |
| // Route to the correct parser or handle as an additional section | |
| if (sectionType === 'title') { | |
| console.log('Identified as TITLE section'); | |
| sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
| if (sectionData) parsedData.title = sectionData; | |
| } else if (sectionType === 'meta') { | |
| console.log('Identified as META section'); | |
| sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
| if (sectionData) parsedData.meta = sectionData; | |
| } else if (sectionType === 'h1') { | |
| console.log('Identified as H1 section'); | |
| sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
| if (sectionData) parsedData.h1 = sectionData; | |
| } else if (sectionType === 'copy') { | |
| console.log('Identified as COPY section'); | |
| sectionData = parseActualQAGuardSection(sectionBlock, sectionType); | |
| if (sectionData) parsedData.copy = sectionData; | |
| } else if (sectionType === 'overall') { | |
| console.log('Identified as OVERALL section'); | |
| const enhancedOverall = parseEnhancedOverallSection(sectionBlock); | |
| parsedData.overall = enhancedOverall; | |
| } else if (header.includes('grades by section') || header.includes('section grades')) { | |
| console.log('Identified as introductory section, skipping.'); | |
| return; | |
| } else { | |
| // Additional sections logic remains here | |
| console.log('Identified as additional section'); | |
| let displayName = headerRaw.replace(/^[#*\s-]+/g, '').trim(); | |
| // ... (existing additional section mapping logic) ... | |
| additionalSections[displayName] = { | |
| content: sectionBlock, | |
| type: determineSectionType(sectionBlock) | |
| }; | |
| } | |
| }); | |
| // If no sections were found, try to extract from the complete text | |
| if (Object.keys(parsedData).length === 0) { | |
| console.log('No sections found, attempting full-text extraction'); | |
| // Try to extract grades and pass status from the raw text | |
| const titleMatch = cleanedQaText.match(/TITLE[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
| const metaMatch = cleanedQaText.match(/META[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
| const h1Match = cleanedQaText.match(/H1[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
| const copyMatch = cleanedQaText.match(/COPY[^:]*:\s*(\d+)\/100[^📋]*?(✅|❌|PASS|FAIL)/i); | |
| if (titleMatch) { | |
| parsedData.title = { | |
| grade: `${titleMatch[1]}/100`, | |
| pass: titleMatch[2] === '✅' || titleMatch[2].toUpperCase() === 'PASS', | |
| errors: titleMatch[2] === '✅' || titleMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
| corrected: 'Content analysis extracted from full report', | |
| rawContent: cleanedQaText | |
| }; | |
| console.log('Extracted TITLE data:', parsedData.title); | |
| } | |
| if (metaMatch) { | |
| parsedData.meta = { | |
| grade: `${metaMatch[1]}/100`, | |
| pass: metaMatch[2] === '✅' || metaMatch[2].toUpperCase() === 'PASS', | |
| errors: metaMatch[2] === '✅' || metaMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
| corrected: 'Content analysis extracted from full report', | |
| rawContent: cleanedQaText | |
| }; | |
| console.log('Extracted META data:', parsedData.meta); | |
| } | |
| if (h1Match) { | |
| parsedData.h1 = { | |
| grade: `${h1Match[1]}/100`, | |
| pass: h1Match[2] === '✅' || h1Match[2].toUpperCase() === 'PASS', | |
| errors: h1Match[2] === '✅' || h1Match[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
| corrected: 'Content analysis extracted from full report', | |
| rawContent: cleanedQaText | |
| }; | |
| console.log('Extracted H1 data:', parsedData.h1); | |
| } | |
| if (copyMatch) { | |
| parsedData.copy = { | |
| grade: `${copyMatch[1]}/100`, | |
| pass: copyMatch[2] === '✅' || copyMatch[2].toUpperCase() === 'PASS', | |
| errors: copyMatch[2] === '✅' || copyMatch[2].toUpperCase() === 'PASS' ? ['No errors reported.'] : ['Violations detected'], | |
| corrected: 'Content analysis extracted from full report', | |
| rawContent: cleanedQaText | |
| }; | |
| console.log('Extracted COPY data:', parsedData.copy); | |
| } | |
| // Extract overall assessment | |
| const overallMatch = cleanedQaText.match(/(?:OVERALL|Final).*?(\d+)\/100/i); | |
| if (overallMatch) { | |
| const overallGrade = parseInt(overallMatch[1]); | |
| parsedData.overall = { | |
| grade: `${overallGrade}/100`, | |
| pass: overallGrade >= 80, | |
| primaryIssue: overallGrade >= 80 ? 'All sections meet requirements' : 'Some violations detected', | |
| rawContent: cleanedQaText | |
| }; | |
| console.log('Extracted OVERALL data:', parsedData.overall); | |
| } | |
| } | |
| const finalReport: DetailedQaReport = { | |
| title: parsedData.title || { ...defaultSection, errors: ['Title section not found in QA response'] }, | |
| meta: parsedData.meta || { ...defaultSection, errors: ['Meta section not found in QA response'] }, | |
| h1: parsedData.h1 || { ...defaultSection, errors: ['H1 section not found in QA response'] }, | |
| copy: parsedData.copy || { ...defaultSection, errors: ['Copy section not found in QA response'] }, | |
| overall: parsedData.overall || { grade: 'N/A', pass: false, primaryIssue: 'Overall assessment not found in QA response' }, | |
| additionalSections: Object.keys(additionalSections).length > 0 ? additionalSections : undefined, | |
| completeRawReport: qaText | |
| }; | |
| // Calculate overall pass/grade from individual sections if overall not found | |
| if (!parsedData.overall && (parsedData.title || parsedData.meta || parsedData.h1 || parsedData.copy)) { | |
| const validSections = [parsedData.title, parsedData.meta, parsedData.h1, parsedData.copy].filter(Boolean); | |
| const allPass = validSections.every(section => section?.pass); | |
| const grades = validSections.map(section => { | |
| if (section?.grade && section.grade !== 'N/A') { | |
| const match = section.grade.match(/(\d+)/); | |
| return match ? parseInt(match[1]) : 0; | |
| } | |
| return 0; | |
| }).filter(g => g > 0); | |
| const avgGrade = grades.length > 0 ? Math.round(grades.reduce((a, b) => a + b, 0) / grades.length) : 0; | |
| finalReport.overall = { | |
| grade: avgGrade > 0 ? `${avgGrade}/100` : 'N/A', | |
| pass: allPass && avgGrade >= 80, | |
| primaryIssue: allPass ? 'All sections passed' : 'Some sections have violations', | |
| rawContent: cleanedQaText | |
| }; | |
| console.log('Calculated overall from sections:', finalReport.overall); | |
| } | |
| console.log('Final QA parsing result:', { | |
| title: finalReport.title?.grade, | |
| meta: finalReport.meta?.grade, | |
| h1: finalReport.h1?.grade, | |
| copy: finalReport.copy?.grade, | |
| overall: finalReport.overall?.grade, | |
| overallPass: finalReport.overall?.pass | |
| }); | |
| return { | |
| detailedQaReport: finalReport, | |
| overallPass: finalReport.overall.pass, | |
| overallGrade: finalReport.overall.grade | |
| }; | |
| }; | |
| /** | |
| * Parses a single section of the QA report (e.g., TITLE, H1) based on the actual QA Guard format. | |
| * This is a more robust parser that handles variations in header format and content. | |
| */ | |
| const parseActualQAGuardSection = (sectionBlock: string, sectionType: string): QaSectionResult | null => { | |
| console.log(`Parsing ${sectionType} section with actual QA Guard format`); | |
| console.log('Section block preview:', sectionBlock.substring(0, 200)); | |
| // Extract grade and pass status from the actual QA Guard format | |
| let grade = 'N/A'; | |
| let pass = false; | |
| let errors: string[] = ['No errors reported.']; | |
| let corrected = 'Content analysis not available.'; | |
| let detailedAssessment = ''; | |
| let keyStrengths: string[] = []; | |
| let recommendations: string[] = []; | |
| let explanations = ''; | |
| // COMPREHENSIVE HEADER PATTERN MATCHING | |
| // Look for the actual QA Guard format: "### **TITLE** ✅ PASS" or "### **TITLE** ❌ FAIL" | |
| // Also handle variations like "## **TITLE: PASS** ✅" or "## **TITLE: FAIL** ❌" | |
| const headerPatterns = [ | |
| /###\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, // ### **TITLE** ✅ PASS | |
| /##\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, // ## **TITLE: PASS** ✅ | |
| /##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)\s*(✅|❌)/i, // ## **TITLE** PASS ✅ | |
| /###\s*\*\*([^*]+):\s*(PASS|FAIL)\*\*\s*(✅|❌)/i, // ### **TITLE: PASS** ✅ | |
| /##\s*\*\*([^*]+)\*\*\s*(✅|❌)\s*(PASS|FAIL)/i, // ## **TITLE** ✅ PASS | |
| /###\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, // ### **TITLE** PASS | |
| /##\s*\*\*([^*]+)\*\*\s*(PASS|FAIL)/i, // ## **TITLE** PASS | |
| /###\s*\*\*([^*]+):\s*(PASS|FAIL)/i, // ### **TITLE: PASS** | |
| /##\s*\*\*([^*]+):\s*(PASS|FAIL)/i, // ## **TITLE: PASS** | |
| /###\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, // ### TITLE ✅ PASS | |
| /##\s*([^*]+)\s*(✅|❌)\s*(PASS|FAIL)/i, // ## TITLE ✅ PASS | |
| /###\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, // ### TITLE: PASS ✅ | |
| /##\s*([^*]+):\s*(PASS|FAIL)\s*(✅|❌)/i, // ## TITLE: PASS ✅ | |
| /###\s*([^*]+)\s*(PASS|FAIL)/i, // ### TITLE PASS | |
| /##\s*([^*]+)\s*(PASS|FAIL)/i, // ## TITLE PASS | |
| /###\s*([^*]+):\s*(PASS|FAIL)/i, // ### TITLE: PASS | |
| /##\s*([^*]+):\s*(PASS|FAIL)/i, // ## TITLE: PASS | |
| ]; | |
| let headerMatch = null; | |
| for (const pattern of headerPatterns) { | |
| headerMatch = sectionBlock.match(pattern); | |
| if (headerMatch) { | |
| console.log('Found QA Guard header format:', headerMatch[0]); | |
| // Determine pass status from various indicators in the match | |
| const passIndicators = [headerMatch[2], headerMatch[3]].filter(Boolean); | |
| pass = passIndicators.some(indicator => | |
| indicator === '✅' || indicator.toUpperCase() === 'PASS' | |
| ); | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE GRADE PATTERN MATCHING | |
| // Look for grade in various formats: "- **Grade:** 100/100", "Grade: 100/100", etc. | |
| const gradePatterns = [ | |
| /-?\s*\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Grade:** 100/100 | |
| /-?\s*\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, // - **Grade**: 100/100 | |
| /-?\s*Grade:\s*(\d+(?:\.\d+)?)\/100/i, // - Grade: 100/100 | |
| /-?\s*Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, // - Grade : 100/100 | |
| /\*\*Grade:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // **Grade:** 100/100 | |
| /\*\*Grade\*\*:\s*(\d+(?:\.\d+)?)\/100/i, // **Grade**: 100/100 | |
| /Grade:\s*(\d+(?:\.\d+)?)\/100/i, // Grade: 100/100 | |
| /Grade\s*:\s*(\d+(?:\.\d+)?)\/100/i, // Grade : 100/100 | |
| /-?\s*\*\*Score:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Score:** 100/100 | |
| /-?\s*\*\*Rating:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Rating:** 100/100 | |
| /-?\s*\*\*Mark:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Mark:** 100/100 | |
| /-?\s*\*\*Points:\*\*\s*(\d+(?:\.\d+)?)\/100/i, // - **Points:** 100/100 | |
| /-?\s*\*\*(\d+(?:\.\d+)?)\/100\s*points?\*\*/i, // - **100/100 points** | |
| /-?\s*(\d+(?:\.\d+)?)\/100\s*points?/i, // - 100/100 points | |
| /-?\s*\*\*(\d+(?:\.\d+)?)\/100\*\*/i, // - **100/100** | |
| /-?\s*(\d+(?:\.\d+)?)\/100/i, // - 100/100 | |
| /\*\*(\d+(?:\.\d+)?)\/100\*\*/i, // **100/100** | |
| /(\d+(?:\.\d+)?)\/100/i, // 100/100 | |
| ]; | |
| let gradeMatch = null; | |
| for (const pattern of gradePatterns) { | |
| gradeMatch = sectionBlock.match(pattern); | |
| if (gradeMatch) { | |
| grade = `${gradeMatch[1]}/100`; | |
| console.log('Found grade:', grade); | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE COMPLIANCE PATTERN MATCHING | |
| // Look for compliance status in various formats | |
| const compliancePatterns = [ | |
| /-?\s*\*\*Compliance:\*\*\s*([^\n]+)/i, // - **Compliance:** Full compliance | |
| /-?\s*\*\*Status:\*\*\s*([^\n]+)/i, // - **Status:** Passed | |
| /-?\s*\*\*Result:\*\*\s*([^\n]+)/i, // - **Result:** Successful | |
| /-?\s*\*\*Assessment:\*\*\s*([^\n]+)/i, // - **Assessment:** Compliant | |
| /-?\s*\*\*Evaluation:\*\*\s*([^\n]+)/i, // - **Evaluation:** Pass | |
| /-?\s*\*\*Check:\*\*\s*([^\n]+)/i, // - **Check:** OK | |
| /-?\s*\*\*Verification:\*\*\s*([^\n]+)/i, // - **Verification:** Valid | |
| /-?\s*\*\*Review:\*\*\s*([^\n]+)/i, // - **Review:** Approved | |
| /-?\s*\*\*Analysis:\*\*\s*([^\n]+)/i, // - **Analysis:** Compliant | |
| /-?\s*\*\*Summary:\*\*\s*([^\n]+)/i, // - **Summary:** Pass | |
| ]; | |
| let complianceMatch = null; | |
| for (const pattern of compliancePatterns) { | |
| complianceMatch = sectionBlock.match(pattern); | |
| if (complianceMatch) { | |
| const compliance = complianceMatch[1].trim(); | |
| console.log('Found compliance:', compliance); | |
| // Determine pass status from compliance text | |
| const failIndicators = ['violation', 'fail', 'error', 'non-compliant', 'rejected', 'invalid', 'incorrect', 'missing', 'below', 'above', 'out of range']; | |
| const passIndicators = ['compliance', 'pass', 'success', 'valid', 'correct', 'approved', 'compliant', 'within range', 'meets', 'satisfies']; | |
| const lowerCompliance = compliance.toLowerCase(); | |
| if (failIndicators.some(indicator => lowerCompliance.includes(indicator))) { | |
| pass = false; | |
| } else if (passIndicators.some(indicator => lowerCompliance.includes(indicator))) { | |
| pass = true; | |
| } | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE ANALYSIS PATTERN MATCHING | |
| // Look for analysis content in various formats | |
| const analysisPatterns = [ | |
| /-?\s*\*\*Analysis:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Analysis:** Detailed text | |
| /-?\s*\*\*Review:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Review:** Detailed text | |
| /-?\s*\*\*Assessment:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Assessment:** Detailed text | |
| /-?\s*\*\*Evaluation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Evaluation:** Detailed text | |
| /-?\s*\*\*Summary:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Summary:** Detailed text | |
| /-?\s*\*\*Details:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Details:** Detailed text | |
| /-?\s*\*\*Breakdown:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Breakdown:** Detailed text | |
| /-?\s*\*\*Explanation:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Explanation:** Detailed text | |
| /-?\s*\*\*Comments:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Comments:** Detailed text | |
| /-?\s*\*\*Notes:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Notes:** Detailed text | |
| ]; | |
| let analysisMatch = null; | |
| for (const pattern of analysisPatterns) { | |
| analysisMatch = sectionBlock.match(pattern); | |
| if (analysisMatch) { | |
| detailedAssessment = analysisMatch[1].trim(); | |
| console.log('Found analysis:', detailedAssessment.substring(0, 100)); | |
| break; | |
| } | |
| } | |
| // COMPREHENSIVE ERROR PATTERN MATCHING | |
| // Look for error information in various formats | |
| const errorPatterns = [ | |
| /-?\s*\*\*Error:\*\*\s*([^\n]+)/i, // - **Error:** Error message | |
| /-?\s*\*\*Errors:\*\*\s*([^\n]+)/i, // - **Errors:** Error messages | |
| /-?\s*\*\*Issue:\*\*\s*([^\n]+)/i, // - **Issue:** Issue description | |
| /-?\s*\*\*Issues:\*\*\s*([^\n]+)/i, // - **Issues:** Issue descriptions | |
| /-?\s*\*\*Problem:\*\*\s*([^\n]+)/i, // - **Problem:** Problem description | |
| /-?\s*\*\*Problems:\*\*\s*([^\n]+)/i, // - **Problems:** Problem descriptions | |
| /-?\s*\*\*Violation:\*\*\s*([^\n]+)/i, // - **Violation:** Violation details | |
| /-?\s*\*\*Violations:\*\*\s*([^\n]+)/i, // - **Violations:** Violation details | |
| /-?\s*\*\*Warning:\*\*\s*([^\n]+)/i, // - **Warning:** Warning message | |
| /-?\s*\*\*Warnings:\*\*\s*([^\n]+)/i, // - **Warnings:** Warning messages | |
| /-?\s*\*\*Concern:\*\*\s*([^\n]+)/i, // - **Concern:** Concern details | |
| /-?\s*\*\*Concerns:\*\*\s*([^\n]+)/i, // - **Concerns:** Concern details | |
| ]; | |
| let errorMatch = null; | |
| for (const pattern of errorPatterns) { | |
| errorMatch = sectionBlock.match(pattern); | |
| if (errorMatch) { | |
| errors = [errorMatch[1].trim()]; | |
| console.log('Found error:', errors[0]); | |
| break; | |
| } | |
| } | |
| // ENHANCED: Look for multi-line violation lists | |
| const violationsMatch = sectionBlock.match(/-?\s*\*\*(?:Structure|Major|Minor)\s+violations?\*\*:\s*([\s\S]*?)(?=\n-?\s*\*\*|\n\n|---|$)/i); | |
| if (violationsMatch) { | |
| const violationText = violationsMatch[1].trim(); | |
| // Split by lines that start with the violation marker | |
| const violationErrors = violationText.split(/\n\s*(?:-|\d+\.)\s*(?:❌|\⚠️)?\s*\*\*/g) | |
| .map(line => { | |
| // Clean up the line to get the core violation text | |
| return line.replace(/MAJOR VIOLATION \([^)]+\):/, '') | |
| .replace(/MINOR VIOLATION \([^)]+\):/, '') | |
| .replace(/\*\*$/, '') | |
| .trim(); | |
| }) | |
| .filter(line => line.length > 10); // Filter out empty or trivial lines | |
| if (violationErrors.length > 0) { | |
| if (errors[0] === 'No errors reported.') { | |
| errors = violationErrors; | |
| } else { | |
| // Prepend violation details to any existing errors | |
| errors = [...violationErrors, ...errors]; | |
| } | |
| console.log('Found detailed violation errors:', violationErrors); | |
| } | |
| } | |
| // COMPREHENSIVE CORRECTED CONTENT PATTERN MATCHING | |
| // Look for corrected content in various formats | |
| const correctedPatterns = [ | |
| /\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **CORRECTED META:**\n```\ncontent\n``` | |
| /\*\*CORRECTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **CORRECTED META:**\ncontent | |
| /\*\*FIXED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **FIXED META:**\n```\ncontent\n``` | |
| /\*\*FIXED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **FIXED META:**\ncontent | |
| /\*\*REVISED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **REVISED META:**\n```\ncontent\n``` | |
| /\*\*REVISED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **REVISED META:**\ncontent | |
| /\*\*UPDATED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **UPDATED META:**\n```\ncontent\n``` | |
| /\*\*UPDATED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **UPDATED META:**\ncontent | |
| /\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **SUGGESTED META:**\n```\ncontent\n``` | |
| /\*\*SUGGESTED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **SUGGESTED META:**\ncontent | |
| /\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n```\n([\s\S]*?)\n```/i, // **RECOMMENDED META:**\n```\ncontent\n``` | |
| /\*\*RECOMMENDED\s+[A-Z]+\*\*:\s*\n([\s\S]*?)(?=\n\*\*|$)/i, // **RECOMMENDED META:**\ncontent | |
| /-?\s*\*\*Corrected:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Corrected:** content | |
| /-?\s*\*\*Fixed:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Fixed:** content | |
| /-?\s*\*\*Revised:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Revised:** content | |
| /-?\s*\*\*Updated:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Updated:** content | |
| /-?\s*\*\*Suggested:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Suggested:** content | |
| /-?\s*\*\*Recommended:\*\*\s*([^\n]+(?:\n(?!-?\s*\*\*)[^\n]+)*)/i, // - **Recommended:** content | |
| ]; | |
| let correctedMatch = null; | |
| for (const pattern of correctedPatterns) { | |
| correctedMatch = sectionBlock.match(pattern); | |
| if (correctedMatch) { | |
| corrected = correctedMatch[1].trim(); | |
| console.log('Found corrected content:', corrected.substring(0, 100)); | |
| break; | |
| } | |
| } | |
| // ENHANCED KEY STRENGTHS AND RECOMMENDATIONS EXTRACTION | |
| // Extract key strengths and recommendations from the analysis and compliance text | |
| if (detailedAssessment) { | |
| // Look for positive indicators in analysis | |
| const positiveIndicators = [ | |
| 'compliance', 'compliant', 'proper', '✓', 'check', 'valid', 'correct', 'appropriate', | |
| 'meets', 'satisfies', 'within range', 'successful', 'approved', 'pass', 'good', 'excellent', | |
| 'optimal', 'ideal', 'perfect', 'complete', 'thorough', 'comprehensive', 'effective' | |
| ]; | |
| const negativeIndicators = [ | |
| 'violation', 'error', 'fail', 'problem', 'issue', 'concern', 'warning', 'missing', | |
| 'below', 'above', 'out of range', 'incorrect', 'invalid', 'non-compliant', 'rejected', | |
| 'insufficient', 'inadequate', 'poor', 'weak', 'deficient' | |
| ]; | |
| const lowerAnalysis = detailedAssessment.toLowerCase(); | |
| // Extract key strengths | |
| if (positiveIndicators.some(indicator => lowerAnalysis.includes(indicator))) { | |
| keyStrengths.push('Meets compliance requirements'); | |
| } | |
| if (lowerAnalysis.includes('keyword') && (lowerAnalysis.includes('included') || lowerAnalysis.includes('present'))) { | |
| keyStrengths.push('Proper keyword integration'); | |
| } | |
| if (lowerAnalysis.includes('tone') && lowerAnalysis.includes('appropriate')) { | |
| keyStrengths.push('Appropriate tone maintained'); | |
| } | |
| if (lowerAnalysis.includes('character') && lowerAnalysis.includes('within')) { | |
| keyStrengths.push('Character count within requirements'); | |
| } | |
| if (lowerAnalysis.includes('length') && lowerAnalysis.includes('✓')) { | |
| keyStrengths.push('Length requirements met'); | |
| } | |
| if (lowerAnalysis.includes('structure') && lowerAnalysis.includes('proper')) { | |
| keyStrengths.push('Proper structure maintained'); | |
| } | |
| // Extract recommendations | |
| if (negativeIndicators.some(indicator => lowerAnalysis.includes(indicator))) { | |
| recommendations.push('Address identified violations'); | |
| } | |
| if (lowerAnalysis.includes('character') && (lowerAnalysis.includes('count') || lowerAnalysis.includes('length'))) { | |
| recommendations.push('Adjust character count to meet requirements'); | |
| } | |
| if (lowerAnalysis.includes('word') && lowerAnalysis.includes('count')) { | |
| recommendations.push('Adjust word count to meet requirements'); | |
| } | |
| if (lowerAnalysis.includes('keyword') && lowerAnalysis.includes('missing')) { | |
| recommendations.push('Include required keywords'); | |
| } | |
| if (lowerAnalysis.includes('tone') && lowerAnalysis.includes('inappropriate')) { | |
| recommendations.push('Adjust tone to meet requirements'); | |
| } | |
| if (lowerAnalysis.includes('structure') && lowerAnalysis.includes('improve')) { | |
| recommendations.push('Improve content structure'); | |
| } | |
| } | |
| // FALLBACK PATTERN MATCHING | |
| // If no grade found, try alternative patterns | |
| if (grade === 'N/A') { | |
| const fallbackGradePatterns = [ | |
| /Grade:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Score:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Rating:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Mark:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /Points:\s*(\d+(?:\.\d+)?)\/100/i, | |
| /(\d+(?:\.\d+)?)\/100/i, | |
| ]; | |
| for (const pattern of fallbackGradePatterns) { | |
| const match = sectionBlock.match(pattern); | |
| if (match) { | |
| grade = `${match[1]}/100`; | |
| console.log('Found fallback grade:', grade); | |
| break; | |
| } | |
| } | |
| } | |
| // INFER PASS STATUS FROM GRADE IF NOT DETERMINED | |
| if (grade !== 'N/A' && !headerMatch && !complianceMatch) { | |
| const gradeNum = parseFloat(grade.split('/')[0]); | |
| pass = gradeNum >= 80; | |
| console.log('Inferred pass status from grade:', pass); | |
| } | |
| // FINAL PASS STATUS DETERMINATION | |
| // If still no pass status, check for pass/fail indicators in text | |
| if (!headerMatch && !complianceMatch && grade === 'N/A') { | |
| const lowerSection = sectionBlock.toLowerCase(); | |
| if (lowerSection.includes('pass') && !lowerSection.includes('fail')) { | |
| pass = true; | |
| } else if (lowerSection.includes('fail')) { | |
| pass = false; | |
| } else if (lowerSection.includes('✅') && !lowerSection.includes('❌')) { | |
| pass = true; | |
| } else if (lowerSection.includes('❌')) { | |
| pass = false; | |
| } | |
| } | |
| // UPDATE ERRORS BASED ON PASS STATUS | |
| if (pass && errors[0] === 'No errors reported.') { | |
| // Keep as is | |
| } else if (!pass && errors[0] === 'No errors reported.') { | |
| errors = ['Violations detected']; | |
| } | |
| console.log(`Final ${sectionType} result - Grade: ${grade}, Pass: ${pass}, Errors: ${errors.length}`); | |
| return { | |
| grade, | |
| pass, | |
| errors, | |
| corrected, | |
| detailedAssessment: detailedAssessment || undefined, | |
| keyStrengths: keyStrengths.length > 0 ? keyStrengths : undefined, | |
| recommendations: recommendations.length > 0 ? recommendations : undefined, | |
| explanations: explanations || undefined, | |
| rawContent: sectionBlock | |
| }; | |
| }; | |
| /** | |
| * Runs the Dify workflow for a given input row, with retries for transient errors. | |
| * @param inputs - The data from a CSV row. | |
| * @returns A promise that resolves to the processed and cleaned results. | |
| */ | |
| export const runWorkflow = async (inputs: ApiInput): Promise<ProcessedResult> => { | |
| let lastError: Error = new Error('Workflow failed after all retries.'); | |
| for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { | |
| let responseText = ''; | |
| try { | |
| const payload = { | |
| inputs, | |
| response_mode: 'streaming', | |
| user: API_USER, | |
| }; | |
| const response = await fetch(API_URL, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${API_TOKEN}`, | |
| 'Content-Type': 'application/json', | |
| }, | |
| body: JSON.stringify(payload), | |
| }); | |
| if (!response.ok) { | |
| responseText = await response.text(); | |
| // Check for retryable HTTP status codes. | |
| if (RETRYABLE_STATUS_CODES.includes(response.status)) { | |
| throw new WorkflowError(`RETRYABLE_HTTP_${response.status}`, `Temporary service issue (HTTP ${response.status}).`, 'network', responseText); | |
| } | |
| // For other HTTP errors, fail immediately. | |
| throw new WorkflowError(`HTTP_${response.status}`, `API request failed (HTTP ${response.status}).`, 'api', responseText); | |
| } | |
| if (!response.body) { | |
| throw new WorkflowError('EMPTY_RESPONSE', 'Empty response from API.', 'network'); | |
| } | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let streamContent = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| streamContent += decoder.decode(value, { stream: true }); | |
| } | |
| // The full stream content becomes our responseText for error logging | |
| responseText = streamContent; | |
| const lines = streamContent.trim().split('\n'); | |
| const finishedLine = lines.find(line => line.includes('"event": "workflow_finished"')) || ''; | |
| if (!finishedLine) { | |
| // The gateway might have returned an HTML error page instead of a stream | |
| if (streamContent.trim().toLowerCase().startsWith('<html')) { | |
| throw new WorkflowError('RETRYABLE_HTML_RESPONSE', 'Service returned an HTML error page.', 'stream', streamContent.slice(0, 1000)); | |
| } | |
| console.error('Full stream content:', streamContent); | |
| throw new WorkflowError('FINISH_EVENT_MISSING', 'Workflow did not finish successfully.', 'stream', streamContent.slice(0, 1000)); | |
| } | |
| const jsonString = finishedLine.replace(/^data: /, ''); | |
| const finishedEventData = JSON.parse(jsonString); | |
| if (finishedEventData.data.status !== 'succeeded') { | |
| const apiError = finishedEventData.data.error || 'Unknown'; | |
| const isOverloaded = typeof apiError === 'string' && (apiError.toLowerCase().includes('overloaded') || apiError.toLowerCase().includes('gateway time-out')); | |
| // If it's a known transient error, mark it as retryable. | |
| if (isOverloaded) { | |
| throw new WorkflowError('RETRYABLE_API_ERROR', 'Service overloaded. Retrying...', 'api', String(apiError)); | |
| } | |
| // Otherwise, it's a permanent failure for this row. | |
| throw new WorkflowError('WORKFLOW_FAILED', `Workflow failed: ${apiError}`, 'api', String(apiError)); | |
| } | |
| const outputs: ApiResponseOutput = finishedEventData.data.outputs; | |
| if (!outputs || Object.keys(outputs).length === 0) { | |
| throw new WorkflowError('EMPTY_OUTPUTS', 'Workflow succeeded but returned empty outputs.', 'parse'); | |
| } | |
| const rawQaReport = outputs.qa_gaurd || 'QA report not available.'; | |
| console.log('QA Report length:', rawQaReport.length); | |
| const { detailedQaReport, overallPass, overallGrade } = parseNewQaReport(rawQaReport); | |
| console.log('Final Parsed QA - Pass:', overallPass, 'Grade:', overallGrade); | |
| // Success, return the result and exit the loop. | |
| return { | |
| generatedTitle: cleanResponseText(outputs.title), | |
| generatedH1: cleanResponseText(outputs.h1), | |
| generatedCopy: cleanResponseText(outputs.copy), | |
| generatedMeta: cleanResponseText(outputs.meta), | |
| qaReport: rawQaReport, | |
| detailedQaReport, | |
| overallPass, | |
| overallGrade, | |
| }; | |
| } catch (error) { | |
| lastError = error instanceof Error ? error : new Error(String(error)); | |
| const isRetryable = lastError instanceof WorkflowError && lastError.code.startsWith('RETRYABLE_'); | |
| if (isRetryable && attempt < MAX_RETRIES) { | |
| // Exponential backoff with jitter: 1s, 2s, 4s, ... + random | |
| const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1); | |
| console.warn(`Attempt ${attempt}/${MAX_RETRIES} failed due to a transient error. Retrying in ~${Math.round(delayMs / 1000)}s...`, { error: lastError.message }); | |
| await delay(delayMs); | |
| continue; // Move to the next attempt | |
| } | |
| // For non-retryable errors, or if we've exhausted retries, break the loop to throw the error. | |
| console.error(`Failed to process row. Attempt ${attempt}/${MAX_RETRIES}. Error: ${lastError.message}`); | |
| if (responseText) { | |
| // Log the problematic response that caused the failure | |
| console.error('Problematic Response:', responseText); | |
| } | |
| break; | |
| } | |
| } | |
| // If the loop finished without returning, it means all attempts failed. | |
| // We re-throw the last captured error, making it more user-friendly if it was a transient one. | |
| if (lastError instanceof WorkflowError && lastError.code.startsWith('RETRYABLE_')) { | |
| throw new WorkflowError('SERVICE_UNAVAILABLE', `API service is temporarily unavailable. Tried ${MAX_RETRIES} times. Please try again later.`, 'api', lastError.debug || lastError.stack); | |
| } | |
| throw lastError; | |
| }; | |