Add URL-based menu import to setup wizard
This commit is contained in:
parent
fccbc17fe3
commit
f6518932db
2 changed files with 535 additions and 18 deletions
368
api/setup/analyzeMenuUrl.cfm
Normal file
368
api/setup/analyzeMenuUrl.cfm
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
<cfsetting showdebugoutput="false">
|
||||
<cfsetting enablecfoutputonly="true">
|
||||
<cfsetting requesttimeout="300">
|
||||
<cfcontent type="application/json; charset=utf-8" reset="true">
|
||||
|
||||
<cfset response = structNew()>
|
||||
<cfset response["OK"] = false>
|
||||
|
||||
<cftry>
|
||||
<!--- Load API Key --->
|
||||
<cfset CLAUDE_API_KEY = "">
|
||||
<cfset configPath = getDirectoryFromPath(getCurrentTemplatePath()) & "../../config/claude.json">
|
||||
<cfif fileExists(configPath)>
|
||||
<cfset configData = deserializeJSON(fileRead(configPath))>
|
||||
<cfif structKeyExists(configData, "apiKey")>
|
||||
<cfset CLAUDE_API_KEY = configData.apiKey>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<cfif NOT len(CLAUDE_API_KEY)>
|
||||
<cfthrow message="Claude API key not configured">
|
||||
</cfif>
|
||||
|
||||
<!--- Get URL from request --->
|
||||
<cfset requestBody = toString(getHttpRequestData().content)>
|
||||
<cfif NOT len(requestBody)>
|
||||
<cfthrow message="No request body provided">
|
||||
</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)>
|
||||
|
||||
<!--- 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>
|
||||
|
||||
<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>
|
||||
|
||||
<!--- Find menu links and fetch them too --->
|
||||
<cfset menuPages = arrayNew(1)>
|
||||
<cfset arrayAppend(menuPages, { url: targetUrl, html: pageHtml })>
|
||||
|
||||
<!--- Look for menu links in the page --->
|
||||
<cfset menuLinkPatterns = 'href=["'']([^"'']*(?:menu|food|dishes|order)[^"'']*)["'']'>
|
||||
<cfset menuLinks = reMatchNoCase(menuLinkPatterns, pageHtml)>
|
||||
|
||||
<cfloop array="#menuLinks#" index="linkMatch">
|
||||
<cfset linkUrl = reReplaceNoCase(linkMatch, 'href=["'']([^"'']*)["'']', "\1")>
|
||||
|
||||
<!--- Resolve relative URLs --->
|
||||
<cfif left(linkUrl, 1) EQ "/">
|
||||
<cfset linkUrl = baseUrl & linkUrl>
|
||||
<cfelseif NOT reFindNoCase("^https?://", linkUrl)>
|
||||
<cfset linkUrl = basePath & linkUrl>
|
||||
</cfif>
|
||||
|
||||
<!--- Skip if same as main page or external domain --->
|
||||
<cfif linkUrl NEQ targetUrl AND findNoCase(baseUrl, linkUrl)>
|
||||
<cftry>
|
||||
<cfhttp url="#linkUrl#" method="GET" timeout="15" result="subPage" 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>
|
||||
|
||||
<cfif findNoCase("200", subPage.statusCode)>
|
||||
<cfset arrayAppend(menuPages, { url: linkUrl, html: subPage.fileContent })>
|
||||
<cfset arrayAppend(response.steps, "Found menu page: " & linkUrl)>
|
||||
</cfif>
|
||||
<cfcatch>
|
||||
<!--- Skip failed requests --->
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
</cfif>
|
||||
|
||||
<!--- Limit to 5 pages max --->
|
||||
<cfif arrayLen(menuPages) GTE 5>
|
||||
<cfbreak>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- Extract images from all pages --->
|
||||
<cfset allImages = arrayNew(1)>
|
||||
<cfset imageUrls = structNew()>
|
||||
|
||||
<cfloop array="#menuPages#" index="menuPage">
|
||||
<!--- Find all img tags --->
|
||||
<cfset imgMatches = reMatchNoCase('<img[^>]+src=["'']([^"'']+)["''][^>]*>', menuPage.html)>
|
||||
|
||||
<cfloop array="#imgMatches#" index="imgTag">
|
||||
<cfset imgSrc = reReplaceNoCase(imgTag, '.*src=["'']([^"'']+)["''].*', "\1")>
|
||||
|
||||
<!--- Resolve relative URLs --->
|
||||
<cfif left(imgSrc, 1) EQ "/">
|
||||
<cfset imgSrc = baseUrl & imgSrc>
|
||||
<cfelseif NOT reFindNoCase("^https?://", imgSrc) AND NOT reFindNoCase("^data:", imgSrc)>
|
||||
<cfset imgSrc = basePath & imgSrc>
|
||||
</cfif>
|
||||
|
||||
<!--- Skip data URLs, icons, and already-processed images --->
|
||||
<cfif reFindNoCase("^https?://", imgSrc) AND NOT structKeyExists(imageUrls, imgSrc)>
|
||||
<!--- Skip common icon/logo patterns that are too small --->
|
||||
<cfif NOT reFindNoCase("(icon|favicon|logo|sprite|pixel|tracking|badge|button)", imgSrc)>
|
||||
<cfset imageUrls[imgSrc] = true>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfloop>
|
||||
|
||||
<cfset arrayAppend(response.steps, "Found #structCount(imageUrls)# unique images")>
|
||||
|
||||
<!--- Download images (limit to 20) --->
|
||||
<cfset imageDataArray = arrayNew(1)>
|
||||
<cfset downloadedCount = 0>
|
||||
|
||||
<cfloop collection="#imageUrls#" item="imgUrl">
|
||||
<cfif downloadedCount GTE 20>
|
||||
<cfbreak>
|
||||
</cfif>
|
||||
|
||||
<cftry>
|
||||
<cfhttp url="#imgUrl#" method="GET" timeout="10" result="imgResult" getasbinary="yes">
|
||||
</cfhttp>
|
||||
|
||||
<cfif findNoCase("200", imgResult.statusCode) AND isBinary(imgResult.fileContent)>
|
||||
<!--- Check content type --->
|
||||
<cfset contentType = structKeyExists(imgResult.responseHeader, "Content-Type") ? imgResult.responseHeader["Content-Type"] : "">
|
||||
|
||||
<cfif reFindNoCase("image/(jpeg|jpg|png|gif|webp)", contentType)>
|
||||
<!--- Check image size (skip tiny images) --->
|
||||
<cfset imgBytes = len(imgResult.fileContent)>
|
||||
|
||||
<cfif imgBytes GT 5000>
|
||||
<cfset base64Content = toBase64(imgResult.fileContent)>
|
||||
|
||||
<cfset mediaType = "image/jpeg">
|
||||
<cfif findNoCase("png", contentType)><cfset mediaType = "image/png"></cfif>
|
||||
<cfif findNoCase("gif", contentType)><cfset mediaType = "image/gif"></cfif>
|
||||
<cfif findNoCase("webp", contentType)><cfset mediaType = "image/webp"></cfif>
|
||||
|
||||
<cfset imgSource = structNew()>
|
||||
<cfset imgSource["type"] = "base64">
|
||||
<cfset imgSource["media_type"] = mediaType>
|
||||
<cfset imgSource["data"] = base64Content>
|
||||
|
||||
<cfset imgStruct = structNew()>
|
||||
<cfset imgStruct["type"] = "image">
|
||||
<cfset imgStruct["source"] = imgSource>
|
||||
<cfset imgStruct["url"] = imgUrl>
|
||||
|
||||
<cfset arrayAppend(imageDataArray, imgStruct)>
|
||||
<cfset downloadedCount = downloadedCount + 1>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<cfcatch>
|
||||
<!--- Skip failed downloads --->
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
</cfloop>
|
||||
|
||||
<cfset arrayAppend(response.steps, "Downloaded #arrayLen(imageDataArray)# valid images")>
|
||||
|
||||
<!--- Combine all page HTML into one text block --->
|
||||
<cfset combinedHtml = "">
|
||||
<cfloop array="#menuPages#" index="menuPage">
|
||||
<!--- Strip scripts, styles, and extract text content --->
|
||||
<cfset cleanHtml = menuPage.html>
|
||||
<cfset cleanHtml = reReplaceNoCase(cleanHtml, "<script[^>]*>.*?</script>", "", "all")>
|
||||
<cfset cleanHtml = reReplaceNoCase(cleanHtml, "<style[^>]*>.*?</style>", "", "all")>
|
||||
<cfset cleanHtml = reReplaceNoCase(cleanHtml, "<!--.*?-->", "", "all")>
|
||||
<cfset combinedHtml = combinedHtml & chr(10) & "--- PAGE: " & menuPage.url & " ---" & chr(10) & cleanHtml>
|
||||
</cfloop>
|
||||
|
||||
<!--- Limit HTML size for Claude --->
|
||||
<cfif len(combinedHtml) GT 100000>
|
||||
<cfset combinedHtml = left(combinedHtml, 100000)>
|
||||
</cfif>
|
||||
|
||||
<!--- System prompt for URL analysis --->
|
||||
<cfset systemPrompt = "You are an expert at extracting structured menu data from restaurant website HTML. Extract ALL menu data visible in the HTML. Return valid JSON with these keys: business (object with name, address, phone, hours, brandColor), categories (array of category names), modifiers (array of modifier templates with name, required boolean, appliesTo, categoryName if applicable, and options array), items (array with name, description, price, category, modifiers array, and imageUrl if found). For brandColor: suggest a vibrant hex color (6 digits, no ##) based on the restaurant style. For hours: format as ""Mon-Fri 10:30am-10pm, Sat 11am-10pm, Sun 11am-9pm"". Include ALL days visible. For prices: extract as numbers (e.g., 12.99). For modifier options: use format {""name"": ""option"", ""price"": 0}. Return ONLY valid JSON, no markdown, no explanation.">
|
||||
|
||||
<!--- Build message content --->
|
||||
<cfset messagesContent = arrayNew(1)>
|
||||
|
||||
<!--- Add images first (up to 10 for analysis) --->
|
||||
<cfset imgLimit = min(arrayLen(imageDataArray), 10)>
|
||||
<cfloop from="1" to="#imgLimit#" index="i">
|
||||
<cfset imgData = imageDataArray[i]>
|
||||
<cfset imgContent = structNew()>
|
||||
<cfset imgContent["type"] = "image">
|
||||
<cfset imgContent["source"] = imgData.source>
|
||||
<cfset arrayAppend(messagesContent, imgContent)>
|
||||
</cfloop>
|
||||
|
||||
<!--- Add HTML text --->
|
||||
<cfset textBlock = structNew()>
|
||||
<cfset textBlock["type"] = "text">
|
||||
<cfset textBlock["text"] = "Extract menu data from this restaurant website HTML. The images above are from the same website - identify which ones are food photos that could be used as item images, and which could be header/banner images. Here is the HTML content:" & chr(10) & chr(10) & combinedHtml>
|
||||
<cfset arrayAppend(messagesContent, textBlock)>
|
||||
|
||||
<cfset userMessage = structNew()>
|
||||
<cfset userMessage["role"] = "user">
|
||||
<cfset userMessage["content"] = messagesContent>
|
||||
|
||||
<cfset requestBody = structNew()>
|
||||
<cfset requestBody["model"] = "claude-sonnet-4-20250514">
|
||||
<cfset requestBody["max_tokens"] = 8192>
|
||||
<cfset requestBody["temperature"] = 0>
|
||||
<cfset requestBody["system"] = systemPrompt>
|
||||
<cfset requestBody["messages"] = arrayNew(1)>
|
||||
<cfset arrayAppend(requestBody["messages"], userMessage)>
|
||||
|
||||
<cfset arrayAppend(response.steps, "Sending to Claude API...")>
|
||||
|
||||
<!--- Call Claude API --->
|
||||
<cfhttp url="https://api.anthropic.com/v1/messages" method="POST" timeout="120" result="httpResult">
|
||||
<cfhttpparam type="header" name="Content-Type" value="application/json">
|
||||
<cfhttpparam type="header" name="x-api-key" value="#CLAUDE_API_KEY#">
|
||||
<cfhttpparam type="header" name="anthropic-version" value="2023-06-01">
|
||||
<cfhttpparam type="body" value="#serializeJSON(requestBody)#">
|
||||
</cfhttp>
|
||||
|
||||
<cfset httpStatusCode = httpResult.statusCode>
|
||||
<cfif isNumeric(httpStatusCode)>
|
||||
<cfset httpStatusCode = int(httpStatusCode)>
|
||||
<cfelseif findNoCase("200", httpStatusCode)>
|
||||
<cfset httpStatusCode = 200>
|
||||
<cfelse>
|
||||
<cfset httpStatusCode = 0>
|
||||
</cfif>
|
||||
|
||||
<cfif httpStatusCode NEQ 200>
|
||||
<cfset errorDetail = "">
|
||||
<cftry>
|
||||
<cfset errorResponse = deserializeJSON(httpResult.fileContent)>
|
||||
<cfif structKeyExists(errorResponse, "error") AND structKeyExists(errorResponse.error, "message")>
|
||||
<cfset errorDetail = errorResponse.error.message>
|
||||
<cfelse>
|
||||
<cfset errorDetail = httpResult.fileContent>
|
||||
</cfif>
|
||||
<cfcatch>
|
||||
<cfset errorDetail = httpResult.fileContent>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
<cfthrow message="Claude API error: #httpResult.statusCode# - #errorDetail#">
|
||||
</cfif>
|
||||
|
||||
<!--- Parse response --->
|
||||
<cfset claudeResponse = deserializeJSON(httpResult.fileContent)>
|
||||
<cfif NOT structKeyExists(claudeResponse, "content") OR NOT arrayLen(claudeResponse.content)>
|
||||
<cfthrow message="Empty response from Claude">
|
||||
</cfif>
|
||||
|
||||
<cfset responseText = "">
|
||||
<cfloop array="#claudeResponse.content#" index="block">
|
||||
<cfif structKeyExists(block, "type") AND block.type EQ "text">
|
||||
<cfset responseText = block.text>
|
||||
<cfbreak>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- Clean up JSON response --->
|
||||
<cfset responseText = trim(responseText)>
|
||||
<cfif left(responseText, 7) EQ "```json">
|
||||
<cfset responseText = mid(responseText, 8, len(responseText) - 7)>
|
||||
</cfif>
|
||||
<cfif left(responseText, 3) EQ "```">
|
||||
<cfset responseText = mid(responseText, 4, len(responseText) - 3)>
|
||||
</cfif>
|
||||
<cfif right(responseText, 3) EQ "```">
|
||||
<cfset responseText = left(responseText, len(responseText) - 3)>
|
||||
</cfif>
|
||||
<cfset responseText = trim(responseText)>
|
||||
<cfset responseText = reReplace(responseText, ",(\s*[\]\}])", "\1", "all")>
|
||||
|
||||
<cfset menuData = deserializeJSON(responseText)>
|
||||
|
||||
<!--- Build image URL list for the wizard to use --->
|
||||
<cfset imageUrlList = arrayNew(1)>
|
||||
<cfloop array="#imageDataArray#" index="imgData">
|
||||
<cfif structKeyExists(imgData, "url")>
|
||||
<cfset arrayAppend(imageUrlList, imgData.url)>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
<!--- Ensure expected structure --->
|
||||
<cfif NOT structKeyExists(menuData, "business")>
|
||||
<cfset menuData["business"] = structNew()>
|
||||
</cfif>
|
||||
<cfif NOT structKeyExists(menuData, "categories")>
|
||||
<cfset menuData["categories"] = arrayNew(1)>
|
||||
</cfif>
|
||||
<cfif NOT structKeyExists(menuData, "modifiers")>
|
||||
<cfset menuData["modifiers"] = arrayNew(1)>
|
||||
</cfif>
|
||||
<cfif NOT structKeyExists(menuData, "items")>
|
||||
<cfset menuData["items"] = arrayNew(1)>
|
||||
</cfif>
|
||||
|
||||
<!--- Convert categories to expected format if needed --->
|
||||
<cfset formattedCategories = arrayNew(1)>
|
||||
<cfloop array="#menuData.categories#" index="cat">
|
||||
<cfif isSimpleValue(cat)>
|
||||
<cfset catObj = structNew()>
|
||||
<cfset catObj["name"] = cat>
|
||||
<cfset catObj["itemCount"] = 0>
|
||||
<cfset arrayAppend(formattedCategories, catObj)>
|
||||
<cfelseif isStruct(cat)>
|
||||
<cfif NOT structKeyExists(cat, "itemCount")>
|
||||
<cfset cat["itemCount"] = 0>
|
||||
</cfif>
|
||||
<cfset arrayAppend(formattedCategories, cat)>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
<cfset menuData["categories"] = formattedCategories>
|
||||
|
||||
<!--- Add item IDs --->
|
||||
<cfloop from="1" to="#arrayLen(menuData.items)#" index="i">
|
||||
<cfset menuData.items[i]["id"] = "item_" & i>
|
||||
</cfloop>
|
||||
|
||||
<!--- Add image URLs to response --->
|
||||
<cfset menuData["imageUrls"] = imageUrlList>
|
||||
<cfset menuData["headerCandidateIndices"] = arrayNew(1)>
|
||||
|
||||
<cfset response["OK"] = true>
|
||||
<cfset response["DATA"] = menuData>
|
||||
<cfset response["sourceUrl"] = targetUrl>
|
||||
<cfset response["pagesProcessed"] = arrayLen(menuPages)>
|
||||
<cfset response["imagesFound"] = arrayLen(imageDataArray)>
|
||||
|
||||
<cfcatch type="any">
|
||||
<cfset response["MESSAGE"] = cfcatch.message>
|
||||
<cfif len(cfcatch.detail)>
|
||||
<cfset response["DETAIL"] = cfcatch.detail>
|
||||
</cfif>
|
||||
<cfif structKeyExists(cfcatch, "tagContext") AND arrayLen(cfcatch.tagContext) GT 0>
|
||||
<cfset response["DEBUG_LINE"] = cfcatch.tagContext[1].line>
|
||||
<cfset response["DEBUG_TEMPLATE"] = cfcatch.tagContext[1].template>
|
||||
</cfif>
|
||||
</cfcatch>
|
||||
</cftry>
|
||||
|
||||
<cfoutput>#serializeJSON(response)#</cfoutput>
|
||||
|
|
@ -769,31 +769,71 @@
|
|||
<!-- Wizard Header -->
|
||||
<div class="wizard-header">
|
||||
<h1>Let's Setup Your Menu</h1>
|
||||
<p>Upload your menu images or PDFs and I'll extract then input all the information for you to preview!</p>
|
||||
<p>Import your menu from a website URL or upload images/PDFs</p>
|
||||
</div>
|
||||
|
||||
<!-- Upload Section -->
|
||||
<div id="uploadSection">
|
||||
<div class="upload-zone" id="uploadZone">
|
||||
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<h3>Drop your menu images here</h3>
|
||||
<p>or click to browse (JPG, PNG, PDF supported)</p>
|
||||
<input type="file" id="fileInput" multiple accept="image/*,.pdf">
|
||||
<!-- Import Method Tabs -->
|
||||
<div class="import-tabs" style="display:flex;gap:0;margin-bottom:20px;border-radius:8px;overflow:hidden;border:1px solid var(--gray-300);">
|
||||
<button class="import-tab active" id="tabUrl" onclick="switchImportTab('url')" style="flex:1;padding:12px 16px;border:none;background:var(--primary);color:white;font-weight:500;cursor:pointer;transition:all 0.2s;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;margin-right:6px;">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
Import from URL
|
||||
</button>
|
||||
<button class="import-tab" id="tabUpload" onclick="switchImportTab('upload')" style="flex:1;padding:12px 16px;border:none;background:var(--gray-100);color:var(--gray-700);font-weight:500;cursor:pointer;transition:all 0.2s;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="vertical-align:middle;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 Files
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="file-preview-grid" id="filePreviewGrid"></div>
|
||||
|
||||
<div class="action-buttons" id="uploadActions" style="display: none;">
|
||||
<button class="btn btn-primary" onclick="startAnalysis()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||
<!-- URL Import Panel -->
|
||||
<div id="urlImportPanel">
|
||||
<div style="background:var(--gray-50);border:2px dashed var(--gray-300);border-radius:12px;padding:32px;text-align:center;">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--gray-400)" stroke-width="1.5" style="margin-bottom:16px;">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
Analyze Menu
|
||||
</button>
|
||||
<h3 style="margin:0 0 8px;color:var(--gray-700);">Enter Restaurant Website URL</h3>
|
||||
<p style="margin:0 0 16px;color:var(--gray-500);font-size:14px;">We'll crawl the site to extract menu items, prices, images, and business info</p>
|
||||
<input type="url" id="menuUrlInput" placeholder="https://restaurant-website.com" style="width:100%;max-width:400px;padding:12px 16px;border:1px solid var(--gray-300);border-radius:8px;font-size:16px;margin-bottom:16px;">
|
||||
<div>
|
||||
<button class="btn btn-primary" onclick="startUrlAnalysis()" 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;">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- File Upload Panel (hidden by default) -->
|
||||
<div id="fileUploadPanel" style="display:none;">
|
||||
<div class="upload-zone" id="uploadZone">
|
||||
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<h3>Drop your menu images here</h3>
|
||||
<p>or click to browse (JPG, PNG, PDF supported)</p>
|
||||
<input type="file" id="fileInput" multiple accept="image/*,.pdf">
|
||||
</div>
|
||||
|
||||
<div class="file-preview-grid" id="filePreviewGrid"></div>
|
||||
|
||||
<div class="action-buttons" id="uploadActions" style="display: none;">
|
||||
<button class="btn btn-primary" onclick="startAnalysis()">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||
</svg>
|
||||
Analyze Menu
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -1209,6 +1249,115 @@
|
|||
}
|
||||
}
|
||||
|
||||
// Switch between URL and file upload tabs
|
||||
function switchImportTab(tab) {
|
||||
const tabUrl = document.getElementById('tabUrl');
|
||||
const tabUpload = document.getElementById('tabUpload');
|
||||
const urlPanel = document.getElementById('urlImportPanel');
|
||||
const filePanel = document.getElementById('fileUploadPanel');
|
||||
|
||||
if (tab === 'url') {
|
||||
tabUrl.style.background = 'var(--primary)';
|
||||
tabUrl.style.color = 'white';
|
||||
tabUpload.style.background = 'var(--gray-100)';
|
||||
tabUpload.style.color = 'var(--gray-700)';
|
||||
urlPanel.style.display = 'block';
|
||||
filePanel.style.display = 'none';
|
||||
} else {
|
||||
tabUpload.style.background = 'var(--primary)';
|
||||
tabUpload.style.color = 'white';
|
||||
tabUrl.style.background = 'var(--gray-100)';
|
||||
tabUrl.style.color = 'var(--gray-700)';
|
||||
urlPanel.style.display = 'none';
|
||||
filePanel.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// URL-based menu import
|
||||
async function startUrlAnalysis() {
|
||||
const urlInput = document.getElementById('menuUrlInput');
|
||||
const url = urlInput.value.trim();
|
||||
|
||||
if (!url) {
|
||||
showToast('Please enter a website URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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>Crawling website and extracting menu data...</span>
|
||||
</div>
|
||||
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">This may take 30-60 seconds while I fetch pages, download images, and analyze everything.</p>
|
||||
`);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.OK) {
|
||||
throw new Error(result.MESSAGE || 'Failed to analyze URL');
|
||||
}
|
||||
|
||||
// Store extracted data
|
||||
config.extractedData = result.DATA;
|
||||
config.sourceUrl = result.sourceUrl;
|
||||
|
||||
// Log debug info
|
||||
console.log('=== URL IMPORT RESPONSE ===');
|
||||
console.log('Source URL:', result.sourceUrl);
|
||||
console.log('Pages processed:', result.pagesProcessed);
|
||||
console.log('Images found:', result.imagesFound);
|
||||
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('URL analysis error:', error);
|
||||
document.getElementById('conversation').innerHTML = '';
|
||||
addMessage('ai', `
|
||||
<p>Sorry, I encountered an error importing from that URL:</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 Files Instead</button>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
function retryUrlAnalysis() {
|
||||
document.getElementById('conversation').innerHTML = '';
|
||||
document.getElementById('uploadSection').style.display = 'block';
|
||||
switchImportTab('url');
|
||||
}
|
||||
|
||||
function switchToFileUpload() {
|
||||
document.getElementById('conversation').innerHTML = '';
|
||||
document.getElementById('uploadSection').style.display = 'block';
|
||||
switchImportTab('upload');
|
||||
}
|
||||
|
||||
async function startAnalysis() {
|
||||
if (config.uploadedFiles.length === 0) {
|
||||
showToast('Please upload at least one menu image', 'error');
|
||||
|
|
|
|||
Reference in a new issue