From 8aeca335fd970c3db3b9bab3536b4ed7ec7153aa Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Fri, 13 Feb 2026 07:02:51 -0800 Subject: [PATCH] Add ZIP upload for saved webpage import For Cloudflare-protected sites, users can now: 1. Save the page from their browser (Webpage, Complete) 2. ZIP the HTML and assets folder 3. Upload the ZIP in the wizard 4. Server extracts to temp folder, Playwright scans local copy Co-Authored-By: Claude Opus 4.5 --- api/setup/uploadSavedPage.cfm | 149 ++++++++++++++++++++++++++ portal/setup-wizard.html | 191 ++++++++++++++++++++-------------- 2 files changed, 264 insertions(+), 76 deletions(-) create mode 100644 api/setup/uploadSavedPage.cfm diff --git a/api/setup/uploadSavedPage.cfm b/api/setup/uploadSavedPage.cfm new file mode 100644 index 0000000..9452e70 --- /dev/null +++ b/api/setup/uploadSavedPage.cfm @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(response)# + + + + + + + + + + + + + + + + + + #serializeJSON(response)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(response)# + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #serializeJSON(response)# + + + + + + + + #serializeJSON(response)# + + diff --git a/portal/setup-wizard.html b/portal/setup-wizard.html index c362be0..80f41b0 100644 --- a/portal/setup-wizard.html +++ b/portal/setup-wizard.html @@ -816,15 +816,16 @@
- -

If the website blocks our crawler, save the menu page and upload it here

- - +

HTML file or ZIP (Save Page As > Webpage, Complete)

@@ -1385,37 +1386,76 @@ switchImportTab('upload'); } - // Handle uploaded HTML file - async function handleHtmlFileUpload(event) { + // Handle uploaded saved page (HTML or ZIP) + async function handleSavedPageUpload(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; + const isZip = file.name.toLowerCase().endsWith('.zip'); - // Hide upload section, show conversation - document.getElementById('uploadSection').style.display = 'none'; + // Hide upload section, show conversation + document.getElementById('uploadSection').style.display = 'none'; - addMessage('ai', ` -
-
- Analyzing saved page: ${file.name}... -
-

Extracting menu data from HTML content.

- `); + addMessage('ai', ` +
+
+ Analyzing saved page: ${file.name}... +
+

${isZip ? 'Uploading and extracting ZIP file...' : 'Extracting menu data from HTML content.'}

+ `); - let result = null; - try { + let result = null; + try { + if (isZip) { + // Upload ZIP file to server for extraction + const formData = new FormData(); + formData.append('zipFile', file); + + const uploadResponse = await fetch(`${config.apiBaseUrl}/setup/uploadSavedPage.cfm`, { + method: 'POST', + body: formData + }); + + const uploadResult = await uploadResponse.json(); + if (!uploadResult.OK) { + throw new Error(uploadResult.MESSAGE || 'Failed to upload ZIP file'); + } + + console.log('ZIP uploaded, extracted URL:', uploadResult.URL); + + // Update loading message + document.getElementById('conversation').innerHTML = ''; + addMessage('ai', ` +
+
+ Scanning extracted page with browser... +
+

Using Playwright to render the saved page and extract menu data.

+ `); + + // Now analyze the extracted URL with Playwright + const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.cfm`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: uploadResult.URL }) + }); + + const responseText = await response.text(); + if (!responseText || responseText.trim().length === 0) { + throw new Error('Server returned empty response'); + } + result = JSON.parse(responseText); + } else { + // Read HTML file content and send directly + const htmlContent = await file.text(); console.log('Sending HTML content, length:', htmlContent.length); + const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ html: htmlContent }) }); - console.log('Response status:', response.status, response.statusText); const responseText = await response.text(); console.log('Raw response (first 500 chars):', responseText.substring(0, 500)); @@ -1429,67 +1469,66 @@ console.error('JSON parse error. Full response:', responseText); throw new Error('Invalid JSON response from server. Check console for details.'); } - - console.log('=== HTML FILE IMPORT RESPONSE ==='); - console.log('File:', file.name); - console.log('Full response:', result); - console.log('================================='); - - if (!result.OK) { - throw new Error(result.MESSAGE || 'Failed to analyze HTML file'); - } - - // Store extracted data - config.extractedData = result.DATA; - - // Store image mappings for matching uploaded images to items - config.imageMappings = result.DATA.imageMappings || []; - console.log('Image mappings from HTML:', config.imageMappings.length); - - // Remove loading message and start conversation flow - document.getElementById('conversation').innerHTML = ''; - - // Check if any items have imageUrl - if so, offer image upload matching - const itemsWithImages = (config.extractedData.items || []).filter(item => item.imageUrl).length; - if (itemsWithImages > 0) { - showImageMatchingStep(); - } else if (config.businessId && config.menuId) { - // In add-menu mode, skip business info and header - showCategoriesStep(); - } else { - showBusinessInfoStep(); - } - - } catch (error) { - console.error('HTML analysis error:', error); - console.error('Full result object:', result); - document.getElementById('conversation').innerHTML = ''; - let debugInfo = ''; - if (result && result.debug) { - debugInfo = `

Debug: hasHtml=${result.debug.hasHtmlKey}, htmlLen=${result.debug.htmlLength}, url="${result.debug.urlValue}"

`; - } - addMessage('ai', ` -

Sorry, I encountered an error analyzing that file:

-

${error.message}

- ${debugInfo} -
- - -
- `); } - }; - reader.onerror = function() { - showToast('Failed to read file', 'error'); - }; + console.log('=== SAVED PAGE IMPORT RESPONSE ==='); + console.log('File:', file.name); + console.log('Full response:', result); + console.log('=================================='); - reader.readAsText(file); + if (!result.OK) { + throw new Error(result.MESSAGE || 'Failed to analyze saved page'); + } + + // Store extracted data + config.extractedData = result.DATA; + + // Store image mappings for matching uploaded images to items + config.imageMappings = result.DATA.imageMappings || []; + console.log('Image mappings from saved page:', config.imageMappings.length); + + // Remove loading message and start conversation flow + document.getElementById('conversation').innerHTML = ''; + + // Check if any items have imageUrl - if so, offer image upload matching + const itemsWithImages = (config.extractedData.items || []).filter(item => item.imageUrl).length; + if (itemsWithImages > 0) { + showImageMatchingStep(); + } else if (config.businessId && config.menuId) { + // In add-menu mode, skip business info and header + showCategoriesStep(); + } else { + showBusinessInfoStep(); + } + + } catch (error) { + console.error('Saved page analysis error:', error); + console.error('Full result object:', result); + document.getElementById('conversation').innerHTML = ''; + let debugInfo = ''; + if (result && result.debug) { + debugInfo = `

Debug: hasHtml=${result.debug.hasHtmlKey}, htmlLen=${result.debug.htmlLength}, url="${result.debug.urlValue}"

`; + } + addMessage('ai', ` +

Sorry, I encountered an error analyzing that file:

+

${error.message}

+ ${debugInfo} +
+ + +
+ `); + } // Reset the input so the same file can be selected again event.target.value = ''; } + // Legacy alias for backwards compatibility + function handleHtmlFileUpload(event) { + handleSavedPageUpload(event); + } + // Show step to upload images from saved webpage subfolder and match to items function showImageMatchingStep() { const itemCount = config.extractedData.items ? config.extractedData.items.length : 0;