Add Uber Eats menu import and fix header image upload step
- Parse JSON-LD structured menu data from saved Uber Eats pages (categories, items, prices, descriptions, business info) - Show save-and-upload instructions when user pastes Uber Eats URL - Always show header image upload step (was skipped for URL imports) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
06adc1211e
commit
49d724f9b2
2 changed files with 190 additions and 7 deletions
|
|
@ -1796,6 +1796,168 @@
|
|||
</cftry>
|
||||
</cfif>
|
||||
|
||||
<!--- ============================================================ --->
|
||||
<!--- UBER EATS FAST PATH: Parse JSON-LD structured menu data --->
|
||||
<!--- ============================================================ --->
|
||||
<cfif findNoCase("ubereats.com", pageHtml) OR findNoCase("uber.com/store", pageHtml)>
|
||||
<cfset arrayAppend(response.steps, "Uber Eats page detected - looking for JSON-LD menu data")>
|
||||
<cftry>
|
||||
<!--- Extract all JSON-LD blocks --->
|
||||
<cfset jsonLdBlocks = reMatchNoCase('<script[^>]*type\s*=\s*["'']application/ld\+json["''][^>]*>([\s\S]*?)</script>', pageHtml)>
|
||||
<cfset arrayAppend(response.steps, "Found " & arrayLen(jsonLdBlocks) & " JSON-LD blocks")>
|
||||
|
||||
<cfset ueRestaurant = "">
|
||||
<cfloop array="#jsonLdBlocks#" index="ldBlock">
|
||||
<!--- Extract content between script tags --->
|
||||
<cfset ldContent = reReplaceNoCase(ldBlock, '<script[^>]*>([\s\S]*?)</script>', '\1')>
|
||||
<cfset ldContent = trim(ldContent)>
|
||||
<cfif len(ldContent)>
|
||||
<cftry>
|
||||
<!--- Unescape unicode \u002F etc --->
|
||||
<cfset ldContent = replace(ldContent, '\u002F', '/', 'all')>
|
||||
<cfset ldContent = replace(ldContent, '\u0026', '&', 'all')>
|
||||
<cfset ldContent = replace(ldContent, '\u0022', '"', 'all')>
|
||||
<cfset ldContent = replace(ldContent, '\u0027', "'", 'all')>
|
||||
<cfset ldParsed = deserializeJSON(ldContent)>
|
||||
<cfif isStruct(ldParsed) AND structKeyExists(ldParsed, "@type") AND ldParsed["@type"] EQ "Restaurant" AND structKeyExists(ldParsed, "hasMenu")>
|
||||
<cfset ueRestaurant = ldParsed>
|
||||
</cfif>
|
||||
<cfcatch><!--- skip unparseable blocks ---></cfcatch>
|
||||
</cftry>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<cfif isStruct(ueRestaurant) AND structKeyExists(ueRestaurant, "hasMenu")>
|
||||
<cfset arrayAppend(response.steps, "Found Restaurant JSON-LD with menu data")>
|
||||
|
||||
<!--- Parse business info --->
|
||||
<cfset ueBusiness = structNew()>
|
||||
<cfset ueBusiness["name"] = structKeyExists(ueRestaurant, "name") ? ueRestaurant.name : "">
|
||||
<!--- Unescape HTML entities in name --->
|
||||
<cfset ueBusiness["name"] = replace(ueBusiness.name, "&", "&", "all")>
|
||||
<cfset ueBusiness["name"] = replace(ueBusiness.name, "<", "<", "all")>
|
||||
<cfset ueBusiness["name"] = replace(ueBusiness.name, ">", ">", "all")>
|
||||
<cfset ueBusiness["name"] = replace(ueBusiness.name, "'", "'", "all")>
|
||||
<cfset ueBusiness["name"] = replace(ueBusiness.name, "'", "'", "all")>
|
||||
|
||||
<cfif structKeyExists(ueRestaurant, "address") AND isStruct(ueRestaurant.address)>
|
||||
<cfset ueAddr = ueRestaurant.address>
|
||||
<cfif structKeyExists(ueAddr, "streetAddress")><cfset ueBusiness["addressLine1"] = ueAddr.streetAddress></cfif>
|
||||
<cfif structKeyExists(ueAddr, "addressLocality")><cfset ueBusiness["city"] = ueAddr.addressLocality></cfif>
|
||||
<cfif structKeyExists(ueAddr, "addressRegion")><cfset ueBusiness["state"] = ueAddr.addressRegion></cfif>
|
||||
<cfif structKeyExists(ueAddr, "postalCode")><cfset ueBusiness["zip"] = ueAddr.postalCode></cfif>
|
||||
</cfif>
|
||||
|
||||
<!--- Parse menu sections --->
|
||||
<cfset ueMenu = ueRestaurant.hasMenu>
|
||||
<cfset ueCategories = []>
|
||||
<cfset ueItems = []>
|
||||
<cfset ueImageMappings = structNew()>
|
||||
<cfset ueItemId = 1>
|
||||
|
||||
<cfif structKeyExists(ueMenu, "hasMenuSection") AND isArray(ueMenu.hasMenuSection)>
|
||||
<cfloop array="#ueMenu.hasMenuSection#" index="ueSection">
|
||||
<cfset ueCatName = structKeyExists(ueSection, "name") ? trim(ueSection.name) : "Menu">
|
||||
<!--- Unescape HTML entities --->
|
||||
<cfset ueCatName = replace(ueCatName, "&", "&", "all")>
|
||||
<cfset ueCatName = replace(ueCatName, "'", "'", "all")>
|
||||
<cfset ueCatName = replace(ueCatName, "'", "'", "all")>
|
||||
<cfset ueSectionItemCount = 0>
|
||||
|
||||
<cfif structKeyExists(ueSection, "hasMenuItem") AND isArray(ueSection.hasMenuItem)>
|
||||
<cfloop array="#ueSection.hasMenuItem#" index="ueMenuItem">
|
||||
<cfset ueItemName = structKeyExists(ueMenuItem, "name") ? trim(ueMenuItem.name) : "">
|
||||
<cfif NOT len(ueItemName)><cfcontinue></cfif>
|
||||
|
||||
<!--- Unescape HTML entities in name and description --->
|
||||
<cfset ueItemName = replace(ueItemName, "&", "&", "all")>
|
||||
<cfset ueItemName = replace(ueItemName, "'", "'", "all")>
|
||||
<cfset ueItemName = replace(ueItemName, "'", "'", "all")>
|
||||
<cfset ueItemName = replace(ueItemName, "<", "<", "all")>
|
||||
<cfset ueItemName = replace(ueItemName, ">", ">", "all")>
|
||||
|
||||
<cfset ueItemDesc = structKeyExists(ueMenuItem, "description") ? trim(ueMenuItem.description) : "">
|
||||
<cfset ueItemDesc = replace(ueItemDesc, "&", "&", "all")>
|
||||
<cfset ueItemDesc = replace(ueItemDesc, "'", "'", "all")>
|
||||
<cfset ueItemDesc = replace(ueItemDesc, "'", "'", "all")>
|
||||
|
||||
<!--- Extract price from offers --->
|
||||
<cfset uePrice = 0>
|
||||
<cfif structKeyExists(ueMenuItem, "offers") AND isStruct(ueMenuItem.offers)>
|
||||
<cfif structKeyExists(ueMenuItem.offers, "price")>
|
||||
<cfset uePrice = val(ueMenuItem.offers.price)>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<!--- Extract image if available --->
|
||||
<cfset ueItemImage = "">
|
||||
<cfif structKeyExists(ueMenuItem, "image") AND len(trim(ueMenuItem.image))>
|
||||
<cfset ueItemImage = trim(ueMenuItem.image)>
|
||||
<cfset ueImageMappings[ueItemName] = ueItemImage>
|
||||
</cfif>
|
||||
|
||||
<cfset arrayAppend(ueItems, {
|
||||
"id": "item_" & ueItemId,
|
||||
"name": ueItemName,
|
||||
"price": uePrice,
|
||||
"description": ueItemDesc,
|
||||
"category": ueCatName,
|
||||
"modifiers": [],
|
||||
"imageUrl": ueItemImage
|
||||
})>
|
||||
<cfset ueItemId = ueItemId + 1>
|
||||
<cfset ueSectionItemCount = ueSectionItemCount + 1>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfif ueSectionItemCount GT 0>
|
||||
<cfset arrayAppend(ueCategories, { "name": ueCatName, "itemCount": ueSectionItemCount })>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<cfset arrayAppend(response.steps, "Parsed " & arrayLen(ueItems) & " items in " & arrayLen(ueCategories) & " categories from Uber Eats JSON-LD")>
|
||||
|
||||
<cfif arrayLen(ueItems) GT 0>
|
||||
<!--- Try to get restaurant header image from JSON-LD images --->
|
||||
<cfset ueHeaderImage = "">
|
||||
<cfif structKeyExists(ueRestaurant, "image") AND isArray(ueRestaurant.image) AND arrayLen(ueRestaurant.image) GT 0>
|
||||
<cfset ueHeaderImage = ueRestaurant.image[1]>
|
||||
</cfif>
|
||||
|
||||
<cfset menuData = {
|
||||
"business": ueBusiness,
|
||||
"categories": ueCategories,
|
||||
"items": ueItems,
|
||||
"modifiers": [],
|
||||
"imageUrls": [],
|
||||
"imageMappings": ueImageMappings,
|
||||
"headerCandidateIndices": [],
|
||||
"headerImage": ueHeaderImage
|
||||
}>
|
||||
|
||||
<cfset response["OK"] = true>
|
||||
<cfset response["DATA"] = menuData>
|
||||
<cfset response["sourceUrl"] = len(targetUrl) ? targetUrl : "ubereats-upload">
|
||||
<cfset response["parsedVia"] = "ubereats_jsonld">
|
||||
<cfset response["pagesProcessed"] = 1>
|
||||
<cfset response["imagesFound"] = structCount(ueImageMappings)>
|
||||
<cfcontent type="application/json" reset="true">
|
||||
<cfoutput>#serializeJSON(response)#</cfoutput>
|
||||
<cfabort>
|
||||
</cfif>
|
||||
<cfelse>
|
||||
<cfset arrayAppend(response.steps, "No Restaurant JSON-LD with menu found - falling through")>
|
||||
</cfif>
|
||||
<cfcatch>
|
||||
<cfset ueError = "Uber Eats JSON-LD parsing failed: " & cfcatch.message>
|
||||
<cfif len(cfcatch.detail)><cfset ueError = ueError & " | Detail: " & cfcatch.detail></cfif>
|
||||
<cfset arrayAppend(response.steps, ueError & " - falling back to Claude")>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
</cfif>
|
||||
<!--- ========== END UBER EATS FAST PATH ========== --->
|
||||
|
||||
<!--- Look for embedded JSON data (Next.js __NEXT_DATA__, Toast state, etc.) --->
|
||||
<cfset embeddedJsonData = "">
|
||||
<cfset embeddedMenuItems = arrayNew(1)>
|
||||
|
|
|
|||
|
|
@ -851,7 +851,7 @@
|
|||
Import Menu
|
||||
</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>
|
||||
<p style="margin:16px 0 0;color:var(--gray-400);font-size:12px;">Works with most restaurant websites, Uber Eats, DoorDash, Grubhub, Toast, Square, and more</p>
|
||||
|
||||
<!-- Divider -->
|
||||
<div style="display:flex;align-items:center;margin:24px 0;gap:16px;">
|
||||
|
|
@ -1356,6 +1356,31 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Uber Eats URLs can't be scraped server-side — prompt user to save page
|
||||
if (url.includes('ubereats.com/store') || url.includes('ubereats.com/store/')) {
|
||||
document.getElementById('uploadSection').style.display = 'none';
|
||||
addMessage('ai', `
|
||||
<p><strong>Uber Eats pages can't be crawled directly</strong> (bot protection), but we can import from a saved page.</p>
|
||||
<ol style="margin:12px 0;padding-left:20px;line-height:1.8;">
|
||||
<li>Open the Uber Eats link in Chrome: <a href="${url}" target="_blank" style="word-break:break-all;">${url}</a></li>
|
||||
<li>Wait for the menu to fully load (scroll down to load all items)</li>
|
||||
<li>Press <strong>Ctrl+S</strong> to save the page as HTML</li>
|
||||
<li>Upload the saved <code>.html</code> file below</li>
|
||||
</ol>
|
||||
<div style="margin-top:16px;">
|
||||
<input type="file" id="uberEatsSavedPage" accept=".html,.htm" style="display:none;" onchange="handleSavedPageUpload(event)">
|
||||
<button class="btn btn-primary" onclick="document.getElementById('uberEatsSavedPage').click()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin-right:6px;">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
Upload Saved Page
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="retryUrlAnalysis()" style="margin-left:8px;">Back</button>
|
||||
</div>
|
||||
`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide upload section, show conversation
|
||||
document.getElementById('uploadSection').style.display = 'none';
|
||||
|
||||
|
|
@ -2272,12 +2297,8 @@
|
|||
hoursSchedule: hoursSchedule // Send the structured schedule instead of the raw hours string
|
||||
};
|
||||
|
||||
// Move to header image step - skip if no auto-detected header (URL imports)
|
||||
if (config.headerImageFile) {
|
||||
showHeaderImageStep();
|
||||
} else {
|
||||
showCategoriesStep();
|
||||
}
|
||||
// Always show header image step so user can upload one
|
||||
showHeaderImageStep();
|
||||
}
|
||||
|
||||
// Header Image step - between business info and categories
|
||||
|
|
|
|||
Reference in a new issue