Add HTML file upload option for menu import

- Backend now accepts either url or html content in request body
- Frontend adds HTML file upload option below URL input
- Useful when websites block the crawler (403 errors)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-02-12 17:13:32 -08:00
parent 31773b0acf
commit 813628cecb
2 changed files with 142 additions and 25 deletions

View file

@ -28,37 +28,57 @@
</cfif>
<cfset requestData = deserializeJSON(requestBody)>
<cfif NOT structKeyExists(requestData, "url") OR NOT len(trim(requestData.url))>
<cfthrow message="URL is required">
</cfif>
<cfset targetUrl = trim(requestData.url)>
<!--- Validate URL format --->
<cfif NOT reFindNoCase("^https?://", targetUrl)>
<cfset targetUrl = "https://" & targetUrl>
</cfif>
<cfset response["steps"] = arrayNew(1)>
<cfset arrayAppend(response.steps, "Fetching URL: " & targetUrl)>
<cfset pageHtml = "">
<cfset baseUrl = "">
<cfset basePath = "">
<cfset targetUrl = "">
<!--- Fetch the main page --->
<cfhttp url="#targetUrl#" method="GET" timeout="30" result="mainPage" useragent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36">
<cfhttpparam type="header" name="Accept" value="text/html,application/xhtml+xml">
</cfhttp>
<!--- Check if HTML content was provided directly (uploaded file or pasted) --->
<cfif structKeyExists(requestData, "html") AND len(trim(requestData.html))>
<cfset pageHtml = trim(requestData.html)>
<cfset arrayAppend(response.steps, "Using provided HTML content: " & len(pageHtml) & " bytes")>
<!--- No base URL for local content - images won't be fetched --->
<cfset baseUrl = "">
<cfset basePath = "">
<cfelseif structKeyExists(requestData, "url") AND len(trim(requestData.url))>
<cfset targetUrl = trim(requestData.url)>
<cfif mainPage.statusCode NEQ "200 OK" AND NOT findNoCase("200", mainPage.statusCode)>
<cfthrow message="Failed to fetch URL: #mainPage.statusCode#">
</cfif>
<!--- Validate URL format --->
<cfif NOT reFindNoCase("^https?://", targetUrl)>
<cfset targetUrl = "https://" & targetUrl>
</cfif>
<cfset pageHtml = mainPage.fileContent>
<cfset arrayAppend(response.steps, "Fetched #len(pageHtml)# bytes")>
<cfset arrayAppend(response.steps, "Fetching URL: " & targetUrl)>
<!--- Extract base URL for resolving relative links --->
<cfset baseUrl = reReplace(targetUrl, "(https?://[^/]+).*", "\1")>
<cfset basePath = reReplace(targetUrl, "(https?://[^/]+/[^?]*/?).*", "\1")>
<cfif NOT reFindNoCase("/$", basePath)>
<cfset basePath = reReplace(basePath, "/[^/]*$", "/")>
<!--- Fetch the main page with browser-like headers --->
<cfhttp url="#targetUrl#" method="GET" timeout="30" result="mainPage" useragent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36">
<cfhttpparam type="header" name="Accept" value="text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8">
<cfhttpparam type="header" name="Accept-Language" value="en-US,en;q=0.9">
<cfhttpparam type="header" name="Accept-Encoding" value="gzip, deflate, br">
<cfhttpparam type="header" name="Sec-Fetch-Dest" value="document">
<cfhttpparam type="header" name="Sec-Fetch-Mode" value="navigate">
<cfhttpparam type="header" name="Sec-Fetch-Site" value="none">
<cfhttpparam type="header" name="Sec-Fetch-User" value="?1">
<cfhttpparam type="header" name="Upgrade-Insecure-Requests" value="1">
</cfhttp>
<cfif mainPage.statusCode NEQ "200 OK" AND NOT findNoCase("200", mainPage.statusCode)>
<cfthrow message="Failed to fetch URL: #mainPage.statusCode#">
</cfif>
<cfset pageHtml = mainPage.fileContent>
<cfset arrayAppend(response.steps, "Fetched " & len(pageHtml) & " bytes")>
<!--- Extract base URL for resolving relative links --->
<cfset baseUrl = reReplace(targetUrl, "(https?://[^/]+).*", "\1")>
<cfset basePath = reReplace(targetUrl, "(https?://[^/]+/[^?]*/?).*", "\1")>
<cfif NOT reFindNoCase("/$", basePath)>
<cfset basePath = reReplace(basePath, "/[^/]*$", "/")>
</cfif>
<cfelse>
<cfthrow message="Either 'url' or 'html' content is required">
</cfif>
<!--- Find menu links and fetch them too --->

View file

@ -808,6 +808,23 @@
</button>
</div>
<p style="margin:16px 0 0;color:var(--gray-400);font-size:12px;">Works with most restaurant websites, DoorDash, Yelp, Toast, Square, and more</p>
<!-- Divider -->
<div style="display:flex;align-items:center;margin:24px 0;gap:16px;">
<div style="flex:1;height:1px;background:var(--gray-300);"></div>
<span style="color:var(--gray-400);font-size:12px;text-transform:uppercase;">or upload saved page</span>
<div style="flex:1;height:1px;background:var(--gray-300);"></div>
</div>
<!-- HTML File Upload -->
<p style="margin:0 0 12px;color:var(--gray-500);font-size:14px;">If the website blocks our crawler, save the menu page and upload it here</p>
<input type="file" id="htmlFileInput" accept=".html,.htm,.mhtml,.txt" style="display:none;" onchange="handleHtmlFileUpload(event)">
<button class="btn btn-secondary" onclick="document.getElementById('htmlFileInput').click()" style="min-width:160px;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>
</svg>
Upload HTML File
</button>
</div>
</div>
@ -1358,6 +1375,86 @@
switchImportTab('upload');
}
// Handle uploaded HTML file
async function handleHtmlFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
// Read the file content
const reader = new FileReader();
reader.onload = async function(e) {
const htmlContent = e.target.result;
// Hide upload section, show conversation
document.getElementById('uploadSection').style.display = 'none';
addMessage('ai', `
<div style="display:flex;align-items:center;gap:12px;">
<div class="loading-spinner"></div>
<span>Analyzing saved page: ${file.name}...</span>
</div>
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">Extracting menu data from HTML content.</p>
`);
try {
const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: htmlContent })
});
const result = await response.json();
if (!result.OK) {
throw new Error(result.MESSAGE || 'Failed to analyze HTML file');
}
// Store extracted data
config.extractedData = result.DATA;
// Log debug info
console.log('=== HTML FILE IMPORT RESPONSE ===');
console.log('File:', file.name);
console.log('Extracted data:', result.DATA);
if (result.steps) {
console.log('Steps:', result.steps);
}
console.log('=================================');
// Remove loading message and start conversation flow
document.getElementById('conversation').innerHTML = '';
// In add-menu mode, skip business info and header
if (config.businessId && config.menuId) {
showCategoriesStep();
} else {
showBusinessInfoStep();
}
} catch (error) {
console.error('HTML analysis error:', error);
document.getElementById('conversation').innerHTML = '';
addMessage('ai', `
<p>Sorry, I encountered an error analyzing that file:</p>
<p style="color: var(--danger);">${error.message}</p>
<div class="action-buttons">
<button class="btn btn-primary" onclick="retryUrlAnalysis()">Try Again</button>
<button class="btn btn-secondary" onclick="switchToFileUpload()">Upload Images Instead</button>
</div>
`);
}
};
reader.onerror = function() {
showToast('Failed to read file', 'error');
};
reader.readAsText(file);
// Reset the input so the same file can be selected again
event.target.value = '';
}
async function startAnalysis() {
if (config.uploadedFiles.length === 0) {
showToast('Please upload at least one menu image', 'error');