import axios from 'axios'; /** * Formats "messed up" text into a structured JSON format using AI. * @param {string} text - The raw text to format. * @param {string} apiKey - The LLM API key. * @param {string} provider - The LLM provider (e.g., 'openai', 'anthropic'). * @returns {Promise} - A structured array of document elements. */ /** * Splits text into chunks based on character count while trying to preserve paragraphs. */ /** * Splits text into chunks based on character count while trying to preserve paragraphs. */ const chunkText = (text, maxLength = 5000) => { const paragraphs = text.split(/\n\n+/); const chunks = []; let currentChunk = ""; for (const p of paragraphs) { if ((currentChunk + p).length > maxLength && currentChunk.length > 0) { chunks.push(currentChunk.trim()); currentChunk = p; } else { currentChunk += (currentChunk ? "\n\n" : "") + p; } } if (currentChunk) chunks.push(currentChunk.trim()); return chunks; }; /** * Formats "messed up" text into a structured JSON format using AI. */ export const formatWithAI = async (text, apiKey, provider = 'openai', onProgress) => { if (!apiKey) return mockFormat(text); const chunks = chunkText(text); let allElements = []; for (let i = 0; i < chunks.length; i++) { if (onProgress) onProgress(i + 1, chunks.length); const chunk = chunks[i]; const prompt = ` You are an expert RFP document formatter. I will provide you with a segment of "messed up" text. Your task is to reformat it into a clean, structured JSON format. Segment ${i + 1} of ${chunks.length}. CRITICAL INSTRUCTIONS (ZERO ADDITION POLICY): - DO NOT ADD ANY INFORMATION THAT IS NOT IN THE SOURCE TEXT. - DO NOT invent answers, do not provide filler text, and do not guess. - DO NOT add greetings, introductions, or concluding remarks. - DO NOT SUMMARIZE OR OMIT ANY TEXT. YOU MUST PROCESS THE ENTIRE SEGMENT. - REPRODUCE EVERY SINGLE PIECE OF INFORMATION FROM THE INPUT. - DO NOT SKIP ANY PARAGRAPHS OR CLAUSES. - RFP RECOGNITION: If you see a question (e.g., text ending in "?" or text that looks like an RFP requirement), format it as an 'heading2' or 'heading3' so it stands out. - If you see a question followed by an answer, the question should be a heading and the answer a paragraph. - Main headers (like "MASTER CONTRACTOR AGREEMENT") must ALWAYS be typed as 'heading1'. - If you see a list of key-value pairs (like Company Name: Value), format it as a 2-column TABLE. - In lists, REMOVE any existing numbering or bullet prefixes (e.g., '(a)', '1.', '-', '•') from the start of each item. - Identify List Style: For 'numberedList', add a 'listType' field. Use 'lowerAlpha' if the source uses (a), (b), (c) or a., b., c. Use 'decimal' if it uses 1, 2, 3. - Identify Centered Titles: If a heading (like "MASTER CONTRACTOR AGREEMENT") should be centered, add an 'alignment' field set to 'center'. Default is 'left'. Return a JSON object with an 'elements' key containing an array of objects. Each object must have: - 'type': one of 'heading1', 'heading2', 'heading3', 'paragraph', 'bulletList', 'numberedList', 'table' - 'listType': (for 'numberedList' only) 'decimal' or 'lowerAlpha' - 'alignment': 'left' or 'center' (default is 'left') - 'content': (string for text, array of strings for lists, array of arrays for tables) Text segment to format: ${chunk} `; let attempts = 0; let success = false; while (attempts < 3 && !success) { attempts++; try { let response; const config = { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json', }, timeout: 90000 // 90s timeout for better reliability }; if (provider === 'openai') { response = await axios.post( 'https://api.openai.com/v1/chat/completions', { model: 'gpt-4o', messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' }, max_tokens: 4096, }, config ); } else if (provider === 'groq') { response = await axios.post( 'https://api.groq.com/openai/v1/chat/completions', { model: 'llama-3.3-70b-versatile', messages: [{ role: 'user', content: prompt }], response_format: { type: 'json_object' }, max_tokens: 4096, }, config ); } const cleanJson = response.data.choices[0].message.content.replace(/```json\n?|\n?```/g, '').trim(); const elements = JSON.parse(cleanJson).elements || []; allElements = [...allElements, ...elements]; success = true; } catch (error) { console.error(`AI Formatting Error on chunk ${i + 1} (Attempt ${attempts}):`, error); if (attempts === 3) { throw new Error(`Failed to format document segment ${i + 1} after 3 attempts. Please try again or check your connection.`); } // Wait 1s before retry await new Promise(resolve => setTimeout(resolve, 1000)); } } } return allElements; }; const mockFormat = (text) => { // Simple mock logic: split by double newlines and guess types const lines = text.split('\n').filter(line => line.trim() !== ''); return lines.map((line, index) => { if (index === 0) return { type: 'heading1', content: line }; if (line.startsWith('* ') || line.startsWith('- ')) { return { type: 'bulletList', content: [line.substring(2)] }; } return { type: 'paragraph', content: line }; }); };