◆ Epps.ai · AI Underwriting
Underwriting
Copilot
◆ Captured Inputs
📊
Drop rent roll or OM here
PDF · Excel · CSV · Accepts rent roll, T-12, OM, budget
◆ Deal Facts
◆ Financials
◆ Value-Add (if applicable)
◆ Deal Context
Underwriting
Copilot
Enter deal inputs or speak your deal. Olesya analyzes returns, risks, value-add observations, and recommends next steps.
IRR · EM · NOI
Risk Assessment
Sensitivity Analysis
Value-Add Observations
IC Memo Generation
LP Narrative
◆ Underwriting Analysis
◆ Olesya Asks
Olesya — Acquisition Underwriting Copilot
◆ Deal Summary
◆ Risk Assessment
◆ Sensitivity Analysis — IRR by Exit Cap & Rent Growth
Gold = base case. Green = above target IRR. Red = below target IRR.
◆ Value-Add Observations
◆ Recommended Next Steps
Operator Review Required · AI-Estimated Returns
IRR, equity multiple, DSCR, and NOI are model-based estimates using institutional benchmark assumptions. These are screening-level figures — not investment-grade underwriting. Validate against a full DCF model before IC presentation or capital commitment. · View governance framework →
◆ Next in Workflow
Continue to IC Memo
Generate investment committee memo →
◆ Generate Documents
Olesya — Generate
IC Memo
Investment Summary
LP Narrative
Risk Memo
Board Update
Ask Olesya
Workflow Copilot · EPPS.AI
Built by an operator. Underwriting · Reporting · Development · Advisory
Olesya Epps · Founder, Epps.ai
Institutional Real Estate · Operator-built AI workflows
Olesya Epps
Workflow Copilot · EPPS.AI Institutional Real Estate Operator-built AI Workflows
About Epps.ai →
Ask Olesya
Hi — I'm Olesya, your Workflow Copilot.

Trained on Epps.ai workflows and real-world real estate operating practices, I can help you navigate underwriting, reporting, development, and investment workflows.

What are you working on today?
"; var blob = new Blob([html],{type:"application/msword"}); var url = URL.createObjectURL(blob); var a = document.createElement("a"); a.href = url; a.download = ((inp.dealName||"Underwriting_Analysis").replace(/[^a-z0-9]/gi,"_")) + ".doc"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function exportWord(){ if(!analysisData){ return; } generateDoc("investment-summary"); setTimeout(exportGeneratedDoc, 200); } // ══════════════════════════════════════════════════════ // VOICE INPUT (same deterministic parser as deal-analyzer) // ══════════════════════════════════════════════════════ var recognition = null; var voiceListening = false; function initVoice(){ var SR = window.SpeechRecognition || window.webkitSpeechRecognition; if(!SR){ document.getElementById("vc-status").textContent = "Voice input is supported in Chrome and Safari. Please enter deal inputs manually."; return; } recognition = new SR(); recognition.continuous = false; recognition.interimResults = true; recognition.lang = "en-US"; recognition.onresult = function(e){ var transcript = Array.from(e.results).map(function(r){return r[0].transcript;}).join(""); document.getElementById("vc-status").textContent = "Listening... " + transcript.slice(-80); if(e.results[e.results.length-1].isFinal) processVoiceInput(transcript); }; recognition.onerror = function(e){ var msg = e.error==="not-allowed" ? "Microphone access was denied. Please enable microphone permissions or enter the deal manually." : e.error==="no-speech" ? "No speech detected. Try again or enter manually." : "Voice input error. Try again."; setVoiceStatus(msg,"err"); setListening(false); }; recognition.onend = function(){ if(voiceListening) setListening(false); }; } function toggleVoice(){ if(!recognition){ initVoice(); if(!recognition) return; } if(voiceListening){ recognition.stop(); setListening(false); } else { recognition.start(); setListening(true); } } function setListening(on){ voiceListening = on; var btn = document.getElementById("vc-btn"); var label = document.getElementById("vc-btn-label"); var waves = document.getElementById("vc-waves"); btn.classList.toggle("listening", on); label.textContent = on ? "Listening..." : "Start Speaking"; waves.style.display = on ? "flex" : "none"; if(on) setVoiceStatus("Listening\u2026 describe the property, units, pricing, rents, cap rate, market, etc.", "active"); } function setVoiceStatus(msg, type){ var el = document.getElementById("vc-status"); if(el) el.textContent = msg; } // (Same deterministic parser as deal-analyzer.html) function normalizeTranscript(t){return t.toLowerCase().replace(/\band\b/g," ").replace(/[,]/g," ").replace(/\bdollars?\b/gi,"").trim();} function parseMoney(t){var m=t.match(/\$?([\d,]+\.?\d*)\s*(million|m)\b/i);if(m)return Math.round(parseFloat(m[1].replace(/,/g,""))*1000000);m=t.match(/\$?([\d,]+\.?\d*)\s*(thousand|k)\b/i);if(m)return Math.round(parseFloat(m[1].replace(/,/g,""))*1000);m=t.match(/\$?([\d,]+\.?\d*)\b/);if(m){var v=parseFloat(m[1].replace(/,/g,""));if(v>100)return Math.round(v);}return null;} function parseUnits(t){var m=t.match(/([\d,]+)\s*-?\s*unit/i);if(m)return parseInt(m[1].replace(/,/g,""));m=t.match(/(\d+)\s*apartment/i);if(m)return parseInt(m[1]);return null;} function parseMarket(t){var m=t.match(/\bin\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?(?:,\s*[A-Z]{2})?)/);if(m)return m[1].trim();var cities=["Phoenix","Austin","Dallas","Atlanta","Charlotte","Nashville","Denver","Tampa","Orlando","Houston","San Antonio","Las Vegas","Reno","Salt Lake City","Boise","Scottsdale","Chandler","Mesa","Tempe","Los Angeles","San Diego","Seattle","Portland","Chicago","Miami","Raleigh","Durham","Greensboro"];var lower=t.toLowerCase();for(var i=0;i=0)return cities[i];}return null;} function setField(id,value){var el=document.getElementById(id);if(!el||value===null||value===undefined||value==="")return false;el.value=String(value);el.classList.add("populated");setTimeout(function(){el.classList.remove("populated");},1200);el.dispatchEvent(new Event("input",{bubbles:true}));el.dispatchEvent(new Event("change",{bubbles:true}));return true;} function processVoiceInput(transcript){ setVoiceStatus("Processing transcript\u2026","active"); var n=normalizeTranscript(transcript); var parsed={}; var filled=0; var units=parseUnits(n);if(units){parsed.units=units+" units";} var market=parseMarket(transcript);if(market){parsed.market=market;} var ppPats=[/purchase price(?:\s+is)?\s+([^.]+)/i,/priced at\s+([^.]+)/i,/asking price(?:\s+is)?\s+([^.]+)/i]; for(var i=0;i100){parsed.inPlaceRent=rent;break;}}} var mrM=transcript.match(/market\s+rent(?:\s+(?:is|of))?\s+([\d,.$]+)/i);if(mrM){var mr=parseFloat(mrM[1].replace(/[$,]/g,""));if(mr>100)parsed.marketRent=mr;} var occM=transcript.match(/([\d.]+)\s*%\s*(?:occupied|occupancy|occ)/i)||transcript.match(/(?:occupancy|occupied)\s+(?:at\s+|of\s+)?([\d.]+)\s*%?/i);if(occM)parsed.occupancy=parseFloat(occM[1]); var ecM=transcript.match(/exit\s+cap(?:\s+(?:rate|of|is))?\s+([\d.]+)/i);if(ecM)parsed.exitCap=parseFloat(ecM[1]); var gcM=transcript.match(/going.in\s+cap(?:\s+(?:rate|of|is))?\s+([\d.]+)/i);if(gcM)parsed.goingInCap=parseFloat(gcM[1]); var ltvM=transcript.match(/([\d.]+)\s*%?\s*(?:loan\s+to\s+value|ltv)/i)||transcript.match(/(?:loan\s+to\s+value|ltv)(?:\s+(?:of|is))?\s+([\d.]+)/i);if(ltvM)parsed.ltv=parseFloat(ltvM[1]); var holdM=transcript.match(/(\d+)\s*(?:\-\s*)?year\s+hold/i)||transcript.match(/hold\s+(?:for\s+)?(\d+)\s*years?/i);if(holdM)parsed.holdPeriod=holdM[1]; var ybM=transcript.match(/(?:year\s+built|built\s+in)\s+((?:19|20)\d{2})/i)||transcript.match(/\b((?:19|20)\d{2})\b/);if(ybM)parsed.vintage=ybM[1]; var fieldMap={units:"units",market:"market",purchasePrice:"purchasePrice",inPlaceRent:"inPlaceRent",marketRent:"marketRent",occupancy:"occupancy",exitCap:"exitCap",goingInCap:"goingInCap",ltv:"ltv",holdPeriod:"holdPeriod",vintage:"vintage"}; Object.keys(fieldMap).forEach(function(k){if(parsed[k]!==undefined){if(setField(fieldMap[k],parsed[k]))filled++;}}); var summaryEl=document.getElementById("voice-summary"); var gridEl=document.getElementById("vs-grid"); var warnEl=document.getElementById("vs-warning"); var transEl=document.getElementById("vs-transcript"); transEl.textContent="\u201c"+transcript+"\u201d"; var labels={units:"Units",market:"Market",purchasePrice:"Purchase Price",inPlaceRent:"In-Place Rent",marketRent:"Market Rent",occupancy:"Occupancy",exitCap:"Exit Cap",goingInCap:"Going-In Cap",ltv:"LTV",holdPeriod:"Hold Period",vintage:"Year Built"}; var rows=""; var cnt=0; Object.keys(labels).forEach(function(k){if(parsed[k]!==undefined){rows+="
"+labels[k]+""+parsed[k]+"
";cnt++;}}); gridEl.innerHTML=rows||"No structured fields detected"; if(cnt===0){warnEl.textContent="No fields could be extracted. Try again with more specific details.";warnEl.style.display="block";}else if(cnt<3){warnEl.textContent="Some fields may need review. Enter additional details manually before analyzing.";warnEl.style.display="block";}else{warnEl.style.display="none";} summaryEl.style.display="block"; setVoiceStatus(cnt>0?"\u2713 "+cnt+" field"+(cnt!==1?"s":"")+" captured \u2014 review and click Analyze":"Could not parse deal \u2014 try again or enter manually",""); } function clearVoice(){ document.getElementById("voice-summary").style.display="none"; document.getElementById("vs-transcript").textContent=""; document.getElementById("vs-grid").innerHTML=""; document.getElementById("vs-warning").style.display="none"; setVoiceStatus("Click the mic and describe your deal naturally.",""); } // UPLOAD // Uploaded file content for analysis context var uploadedFileContent = null; var uploadedFileName = null; function handleFileUpload(input){ if(!input.files||!input.files.length) return; var file = input.files[0]; var ext = file.name.split('.').pop().toLowerCase(); var label = document.getElementById("uploaded-file-label"); label.style.display = "block"; label.textContent = "⟳ Reading " + file.name + "…"; uploadedFileName = file.name; var zone = document.getElementById("upload-zone"); zone.style.borderColor = "rgba(201,168,76,0.5)"; function onParsed(text) { uploadedFileContent = text; label.textContent = "✓ " + file.name + " (" + Math.round(file.size/1024) + "KB parsed) — Pre-populating fields…"; zone.style.borderColor = "rgba(45,189,126,0.5)"; setTimeout(function(){zone.style.borderColor="";}, 2000); // Pre-populate fields from extracted content extractAndPopulate(text, file.name, ext); } if (['xlsx','xls'].includes(ext)) { var reader = new FileReader(); reader.onload = function(e) { try { var wb = XLSX.read(e.target.result, {type:'array', sheetRows:300}); var text = ''; wb.SheetNames.slice(0,6).forEach(function(sn){ text += '=== ' + sn + ' ===\n' + XLSX.utils.sheet_to_csv(wb.Sheets[sn], {RS:'\n',FS:'\t'}) + '\n\n'; }); onParsed(text.substring(0, 8000)); } catch(err) { onParsed(''); label.textContent = "⚠ " + file.name + " — Could not parse. Enter fields manually."; } }; reader.readAsArrayBuffer(file); } else if (ext === 'csv') { var reader = new FileReader(); reader.onload = function(e){ onParsed((e.target.result||'').substring(0, 6000)); }; reader.readAsText(file); } else if (ext === 'pdf') { var reader = new FileReader(); reader.onload = function(e) { var bytes = new Uint8Array(e.target.result); var str = ''; for (var i=0; i1) extracted+=cl+' '; }); }); onParsed((extracted.length>200?extracted:str.replace(/[^\x20-\x7E\n]/g,' ')).substring(0,6000)); }; reader.readAsArrayBuffer(file); } else { // .docx/.txt — read as text var reader = new FileReader(); reader.onload = function(e){ var raw = e.target.result || ''; var clean = raw.replace(/<[^>]+>/g,' ').replace(/[\x00-\x1F\x7F]/g,' ').replace(/\s+/g,' '); onParsed(clean.substring(0,6000)); }; reader.readAsBinaryString ? reader.readAsBinaryString(file) : reader.readAsText(file); } // Always pre-fill deal name from filename if(!document.getElementById("dealName").value) { document.getElementById("dealName").value = file.name.replace(/\.[^.]+$/,'').replace(/[-_]/g,' '); } } function extractAndPopulate(text, filename, ext) { var t = text.toLowerCase(); var setField = function(id, val) { var el = document.getElementById(id); if (el && !el.value && val) el.value = val; }; // Unit count — look for "X units" or "X-unit" patterns var unitsMatch = text.match(/(\d{2,4})\s*(?:-\s*)?units?\b/i) || text.match(/total\s+units?[:\s]+(\d+)/i) || text.match(/number of units?[:\s]+(\d+)/i); if (unitsMatch) setField('units', unitsMatch[1]); // Occupancy — "XX% occupied" or "occupancy: XX%" var occMatch = text.match(/occup(?:ancy|ied)[:\s]+([\d.]+)%/i) || text.match(/([\d.]+)%\s*(?:occupied|occupancy)/i) || text.match(/occupancy[\s\S]{0,30}([\d.]+)%/i); if (occMatch) setField('occupancy', occMatch[1]); // Average rent — "$X,XXX" near "avg rent" or "market rent" var rentMatch = text.match(/avg\.?\s*rent[:\s]+\$?([\d,]+)/i) || text.match(/average\s+rent[:\s]+\$?([\d,]+)/i) || text.match(/in.place\s+rent[:\s]+\$?([\d,]+)/i) || text.match(/\$([1-9]\d{2,3})\s*\/\s*(?:mo|month|unit)/i); if (rentMatch) setField('inPlaceRent', rentMatch[1].replace(/,/g,'')); // Market rent var mktRentMatch = text.match(/market\s+rent[:\s]+\$?([\d,]+)/i) || text.match(/asking\s+rent[:\s]+\$?([\d,]+)/i); if (mktRentMatch) setField('marketRent', mktRentMatch[1].replace(/,/g,'')); // NOI — "NOI: $X,XXX,XXX" or "net operating income: $X" var noiMatch = text.match(/\bNOI[:\s]+\$?([\d,]+(?:\.\d+)?(?:[MK])?)/i) || text.match(/net operating income[:\s]+\$?([\d,]+)/i); if (noiMatch) { // Convert to annual number if it seems monthly var noi = noiMatch[1].replace(/,/g,''); // Not pre-populating NOI directly but useful for context } // Cap rate — "X.X% cap" or "cap rate: X.X%" var capMatch = text.match(/cap\s+rate[:\s]+([\d.]+)%/i) || text.match(/([\d.]+)%\s*cap(?:\s+rate)?\b/i) || text.match(/going.in\s+cap[:\s]+([\d.]+)%/i); if (capMatch && parseFloat(capMatch[1]) < 15) setField('goingInCap', capMatch[1]); // Purchase price — "$XX.XM" or "$XX,XXX,XXX" var ppMatch = text.match(/(?:purchase|asking|list|sale)\s+price[:\s]+\$?([\d,]+(?:\.\d+)?(?:[Mm])?)/i) || text.match(/\$([1-9]\d{0,2}(?:,\d{3}){1,3})(?:\s*purchase)/i); if (ppMatch) { var pp = ppMatch[1].replace(/,/g,''); if (/[Mm]$/.test(pp)) pp = parseFloat(pp) * 1000000; setField('purchasePrice', pp); } // Market / location var mktMatch = text.match(/(?:located in|market|city)[:\s]+([A-Z][a-zA-Z\s]+,\s*[A-Z]{2}\b)/i) || text.match(/([A-Z][a-zA-Z\s]+,\s*[A-Z]{2})\s*(?:market|msa|metro)/i); if (mktMatch) setField('market', mktMatch[1].trim()); // LTV / leverage var ltvMatch = text.match(/LTV[:\s]+([\d.]+)%/i) || text.match(/([\d.]+)%\s*LTV/i) || text.match(/leverage[:\s]+([\d.]+)%/i); if (ltvMatch && parseFloat(ltvMatch[1]) < 85) setField('ltv', ltvMatch[1]); // Interest rate var irMatch = text.match(/(?:interest|coupon)\s+rate[:\s]+([\d.]+)%/i) || text.match(/([\d.]+)%\s*(?:interest|coupon)/i); if (irMatch && parseFloat(irMatch[1]) < 15) setField('interestRate', irMatch[1]); // Update label with what was extracted var extracted = []; if (unitsMatch) extracted.push('units'); if (occMatch) extracted.push('occupancy'); if (rentMatch) extracted.push('rent'); if (capMatch) extracted.push('cap rate'); if (ppMatch) extracted.push('purchase price'); if (ltvMatch) extracted.push('LTV'); var label = document.getElementById("uploaded-file-label"); if (extracted.length > 0) { label.textContent = "✓ " + filename + " — Extracted: " + extracted.join(', ') + ". Review and adjust fields as needed."; label.style.color = "var(--green)"; } else { label.textContent = "✓ " + filename + " uploaded. No auto-extractable fields found — enter details manually."; label.style.color = "var(--mist)"; } label.style.display = 'block'; } // DRAG AND DROP document.addEventListener("DOMContentLoaded",function(){ var uz=document.getElementById("upload-zone"); if(uz){ uz.addEventListener("dragover",function(e){e.preventDefault();uz.classList.add("drag-over");}); uz.addEventListener("dragleave",function(){uz.classList.remove("drag-over");}); uz.addEventListener("drop",function(e){e.preventDefault();uz.classList.remove("drag-over");var fi=document.getElementById("file-input");if(e.dataTransfer.files.length){var dt=new DataTransfer();dt.items.add(e.dataTransfer.files[0]);fi.files=dt.files;handleFileUpload(fi);}}); } initVoice(); setState("empty"); });