Add subcategory detection to wizard URL analyzer and display
- analyzeMenuUrl.cfm: Detect subcategories from Toast subgroups and Claude API responses, preserve hierarchy with parentCategoryName - setup-wizard.html: Display subcategories indented under parents throughout wizard flow (categories step, items review, summary, preview) - menu-builder.html: Show subcategories nested in outline modal view Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3ccc82c9f2
commit
495b03c76d
3 changed files with 224 additions and 96 deletions
|
|
@ -335,6 +335,71 @@
|
|||
<cfset arrayAppend(toastCategories, { "name": groupName, "itemCount": 0 })>
|
||||
</cfif>
|
||||
</cfif>
|
||||
|
||||
<!--- Debug: log group keys to help identify subgroup field names --->
|
||||
<cfif isStruct(group) AND NOT structKeyExists(variables, "loggedGroupKeys")>
|
||||
<cfset variables.loggedGroupKeys = true>
|
||||
<cfset arrayAppend(response.steps, "Group keys: " & structKeyList(group))>
|
||||
</cfif>
|
||||
|
||||
<!--- Check for subgroups (nested categories within this group) --->
|
||||
<!--- Try multiple field names: subgroups, children, childGroups --->
|
||||
<cfset subgroupsArr = arrayNew(1)>
|
||||
<cfif structKeyExists(group, "subgroups") AND isArray(group.subgroups)>
|
||||
<cfset subgroupsArr = group.subgroups>
|
||||
<cfelseif structKeyExists(group, "children") AND isArray(group.children)>
|
||||
<cfset subgroupsArr = group.children>
|
||||
<cfelseif structKeyExists(group, "childGroups") AND isArray(group.childGroups)>
|
||||
<cfset subgroupsArr = group.childGroups>
|
||||
</cfif>
|
||||
<cfset hasSubgroups = false>
|
||||
<cfif arrayLen(subgroupsArr) GT 0>
|
||||
<cfset hasSubgroups = true>
|
||||
<cfset arrayAppend(response.steps, "Group '" & groupName & "' has " & arrayLen(subgroupsArr) & " subgroups")>
|
||||
<cfloop array="#subgroupsArr#" index="subgroup">
|
||||
<cfset subgroupName = "">
|
||||
<cfif structKeyExists(subgroup, "name") AND len(trim(subgroup.name))>
|
||||
<cfset subgroupName = trim(subgroup.name)>
|
||||
<cfif NOT structKeyExists(categorySet, subgroupName)>
|
||||
<cfset categorySet[subgroupName] = true>
|
||||
<cfset arrayAppend(toastCategories, { "name": subgroupName, "parentCategoryName": groupName, "itemCount": 0 })>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<!--- Extract items from subgroup --->
|
||||
<cfif structKeyExists(subgroup, "items") AND isArray(subgroup.items)>
|
||||
<cfset effectiveName = len(subgroupName) ? subgroupName : groupName>
|
||||
<cfloop array="#subgroup.items#" index="item">
|
||||
<cfif structKeyExists(item, "name")>
|
||||
<cfset itemCategoryMap[item.name] = effectiveName>
|
||||
<!--- Extract price --->
|
||||
<cfif structKeyExists(item, "price") AND isNumeric(item.price)>
|
||||
<cfset itemPriceMap[item.name] = val(item.price)>
|
||||
<cfelseif structKeyExists(item, "unitPrice") AND isNumeric(item.unitPrice)>
|
||||
<cfset itemPriceMap[item.name] = val(item.unitPrice)>
|
||||
<cfelseif structKeyExists(item, "basePrice") AND isNumeric(item.basePrice)>
|
||||
<cfset itemPriceMap[item.name] = val(item.basePrice)>
|
||||
<cfelseif structKeyExists(item, "displayPrice")>
|
||||
<cfset priceStr = reReplace(item.displayPrice, "[^0-9.]", "", "all")>
|
||||
<cfif len(priceStr) AND isNumeric(priceStr)>
|
||||
<cfset itemPriceMap[item.name] = val(priceStr)>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<!--- Extract image URLs --->
|
||||
<cfif structKeyExists(item, "imageUrls")>
|
||||
<cfset imgUrls = item.imageUrls>
|
||||
<cfif structKeyExists(imgUrls, "medium")>
|
||||
<cfset imageMap[item.name] = imgUrls.medium>
|
||||
<cfelseif structKeyExists(imgUrls, "large")>
|
||||
<cfset imageMap[item.name] = imgUrls.large>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
||||
<!--- Extract direct items from group (not in subgroups) --->
|
||||
<cfif structKeyExists(group, "items") AND isArray(group.items)>
|
||||
<!--- Debug: log first item's structure --->
|
||||
<cfif arrayLen(group.items) GT 0 AND NOT structKeyExists(variables, "loggedItemKeys")>
|
||||
|
|
@ -342,7 +407,6 @@
|
|||
<cfset firstItem = group.items[1]>
|
||||
<cfif isStruct(firstItem)>
|
||||
<cfset arrayAppend(response.steps, "First item keys: " & structKeyList(firstItem))>
|
||||
<!--- Log a few specific values for debugging --->
|
||||
<cfif structKeyExists(firstItem, "price")>
|
||||
<cfset arrayAppend(response.steps, "item.price = " & firstItem.price)>
|
||||
</cfif>
|
||||
|
|
@ -973,7 +1037,7 @@
|
|||
<cfset arrayAppend(response.steps, "Found " & arrayLen(h3Texts) & " h3 and " & arrayLen(h4Texts) & " h4 tags")>
|
||||
|
||||
<!--- 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), items (array with name, description, price, category, subcategory, modifiers array, and imageUrl). CRITICAL FOR IMAGES: Each menu item in the HTML is typically in a container (div, li, article) that also contains an img tag. Extract the img src URL and include it as 'imageUrl' for that item. Look for img tags that are siblings or children within the same menu-item container. The image URL should be the full or relative src value from the img tag - NOT the alt text. CRITICAL: Extract EVERY menu item from ALL sources including embedded JSON (__NEXT_DATA__, window state, JSON-LD). SUBCATEGORY RULE: If a section header has NO menu items directly below it but contains NESTED sections, the outer section is the PARENT CATEGORY and inner sections are SUBCATEGORIES. For items in subcategories, set category to the PARENT name and subcategory to the inner section name. For brandColor: suggest a vibrant hex (6 digits, no hash). For prices: numbers (e.g., 12.99). Return ONLY valid JSON.">
|
||||
<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), modifiers (array), items (array with name, description, price, category, modifiers array, and imageUrl). CATEGORIES FORMAT: Each entry in the categories array can be either a simple string (for flat categories) OR an object with 'name' and optional 'subcategories' array. Example: [""Appetizers"", {""name"": ""Drinks"", ""subcategories"": [""Hot Drinks"", ""Cold Drinks""]}, ""Desserts""]. SUBCATEGORY DETECTION: If a section header contains nested titled sections beneath it (sub-headers with their own items), the outer section is the PARENT and inner sections are SUBCATEGORIES. For items in subcategories, set their 'category' field to the SUBCATEGORY name (not the parent). CRITICAL FOR IMAGES: Each menu item in the HTML is typically in a container (div, li, article) that also contains an img tag. Extract the img src URL and include it as 'imageUrl' for that item. Look for img tags that are siblings or children within the same menu-item container. The image URL should be the full or relative src value from the img tag - NOT the alt text. CRITICAL: Extract EVERY menu item from ALL sources including embedded JSON (__NEXT_DATA__, window state, JSON-LD). For brandColor: suggest a vibrant hex (6 digits, no hash). For prices: numbers (e.g., 12.99). Return ONLY valid JSON.">
|
||||
|
||||
<!--- Build message content --->
|
||||
<cfset messagesContent = arrayNew(1)>
|
||||
|
|
@ -1113,9 +1177,8 @@
|
|||
<cfset menuData["items"] = arrayNew(1)>
|
||||
</cfif>
|
||||
|
||||
<!--- Convert categories to expected format - flatten subcategories into parent --->
|
||||
<!--- Convert categories to expected format - preserve subcategory hierarchy --->
|
||||
<cfset formattedCategories = arrayNew(1)>
|
||||
<cfset subcatToParentMap = structNew()><!--- Map subcategory names to parent category names --->
|
||||
<cfloop array="#menuData.categories#" index="cat">
|
||||
<cfif isSimpleValue(cat)>
|
||||
<cfset catObj = structNew()>
|
||||
|
|
@ -1123,14 +1186,13 @@
|
|||
<cfset catObj["itemCount"] = 0>
|
||||
<cfset arrayAppend(formattedCategories, catObj)>
|
||||
<cfelseif isStruct(cat)>
|
||||
<!--- Add only the parent category --->
|
||||
<cfset parentName = structKeyExists(cat, "name") ? cat.name : "">
|
||||
<cfif len(parentName)>
|
||||
<cfset catObj = structNew()>
|
||||
<cfset catObj["name"] = parentName>
|
||||
<cfset catObj["itemCount"] = 0>
|
||||
<cfset arrayAppend(formattedCategories, catObj)>
|
||||
<!--- Build map of subcategory names -> parent name for item reassignment --->
|
||||
<!--- Add subcategories with parentCategoryName --->
|
||||
<cfif structKeyExists(cat, "subcategories") AND isArray(cat.subcategories)>
|
||||
<cfloop array="#cat.subcategories#" index="subcat">
|
||||
<cfset subcatName = "">
|
||||
|
|
@ -1140,7 +1202,11 @@
|
|||
<cfset subcatName = subcat.name>
|
||||
</cfif>
|
||||
<cfif len(subcatName)>
|
||||
<cfset subcatToParentMap[lcase(subcatName)] = parentName>
|
||||
<cfset subcatObj = structNew()>
|
||||
<cfset subcatObj["name"] = subcatName>
|
||||
<cfset subcatObj["parentCategoryName"] = parentName>
|
||||
<cfset subcatObj["itemCount"] = 0>
|
||||
<cfset arrayAppend(formattedCategories, subcatObj)>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
</cfif>
|
||||
|
|
@ -1149,22 +1215,12 @@
|
|||
</cfloop>
|
||||
<cfset menuData["categories"] = formattedCategories>
|
||||
|
||||
<!--- Reassign items in subcategories to their parent category --->
|
||||
<!--- For items with subcategory field from Claude, set their category to the subcategory name --->
|
||||
<cfloop from="1" to="#arrayLen(menuData.items)#" index="i">
|
||||
<cfset item = menuData.items[i]>
|
||||
<!--- Check if item's category is actually a subcategory --->
|
||||
<cfif structKeyExists(item, "category") AND len(item.category)>
|
||||
<cfset catKey = lcase(item.category)>
|
||||
<cfif structKeyExists(subcatToParentMap, catKey)>
|
||||
<cfset menuData.items[i]["category"] = subcatToParentMap[catKey]>
|
||||
</cfif>
|
||||
</cfif>
|
||||
<!--- Also check subcategory field if present --->
|
||||
<!--- If Claude set a subcategory field, use that as the item's category --->
|
||||
<cfif structKeyExists(item, "subcategory") AND len(item.subcategory)>
|
||||
<cfset subcatKey = lcase(item.subcategory)>
|
||||
<cfif structKeyExists(subcatToParentMap, subcatKey)>
|
||||
<cfset menuData.items[i]["category"] = subcatToParentMap[subcatKey]>
|
||||
</cfif>
|
||||
<cfset menuData.items[i]["category"] = item.subcategory>
|
||||
</cfif>
|
||||
</cfloop>
|
||||
|
||||
|
|
@ -1232,8 +1288,6 @@
|
|||
<cfset response["pagesProcessed"] = arrayLen(menuPages)>
|
||||
<cfset response["imagesFound"] = arrayLen(imageDataArray)>
|
||||
<cfset response["playwrightImagesCount"] = arrayLen(playwrightImages)>
|
||||
<!--- Debug: show subcategory mapping --->
|
||||
<cfset response["DEBUG_SUBCAT_MAP"] = subcatToParentMap>
|
||||
<cfset response["DEBUG_PLAYWRIGHT_IMAGES"] = playwrightImages>
|
||||
<cfset response["DEBUG_RAW_CATEGORIES"] = menuData.categories>
|
||||
|
||||
|
|
|
|||
|
|
@ -3383,16 +3383,29 @@
|
|||
} else {
|
||||
outline = '<div class="menu-outline">';
|
||||
|
||||
for (const cat of categories) {
|
||||
outline += `<div class="outline-category">${this.escapeHtml(cat.name)}</div>`;
|
||||
const topLevel = categories.filter(c => !c.parentCategoryId || c.parentCategoryId === 0);
|
||||
const getSubcats = (parent) => categories.filter(c =>
|
||||
c.parentCategoryId === parent.id || (parent.dbId && c.parentCategoryDbId === parent.dbId)
|
||||
);
|
||||
|
||||
const items = cat.items || [];
|
||||
for (const item of items) {
|
||||
const renderOutlineItems = (items, indentLevel) => {
|
||||
let html = '';
|
||||
for (const item of (items || [])) {
|
||||
const itemPrice = item.price ? `$${parseFloat(item.price).toFixed(2)}` : '';
|
||||
outline += `<div class="outline-item">${this.escapeHtml(item.name)}${itemPrice ? ' <span class="outline-price">' + itemPrice + '</span>' : ''}</div>`;
|
||||
html += `<div class="outline-item" style="padding-left: ${indentLevel * 20}px;">${this.escapeHtml(item.name)}${itemPrice ? ' <span class="outline-price">' + itemPrice + '</span>' : ''}</div>`;
|
||||
html += this.renderOutlineModifiers(item.modifiers || [], indentLevel + 1);
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
// Render modifiers recursively
|
||||
outline += this.renderOutlineModifiers(item.modifiers || [], 2);
|
||||
for (const cat of topLevel) {
|
||||
outline += `<div class="outline-category">${this.escapeHtml(cat.name)}</div>`;
|
||||
outline += renderOutlineItems(cat.items, 1);
|
||||
|
||||
const subcats = getSubcats(cat);
|
||||
for (const subcat of subcats) {
|
||||
outline += `<div class="outline-subcategory">${this.escapeHtml(subcat.name)}</div>`;
|
||||
outline += renderOutlineItems(subcat.items, 2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3422,6 +3435,14 @@
|
|||
.outline-category:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.outline-subcategory {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 8px;
|
||||
margin-bottom: 2px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.outline-item {
|
||||
padding-left: 20px;
|
||||
color: var(--text-primary);
|
||||
|
|
|
|||
|
|
@ -2289,13 +2289,13 @@
|
|||
}
|
||||
|
||||
// Step 2: Categories
|
||||
function showCategoriesStep() {
|
||||
updateProgress(3);
|
||||
const categories = config.extractedData.categories || [];
|
||||
|
||||
let categoriesHtml = categories.map((cat, i) => `
|
||||
<div class="extracted-list-item">
|
||||
function renderCategoryListHtml(categories) {
|
||||
return categories.map((cat, i) => {
|
||||
const isSubcat = cat.parentCategoryName ? true : false;
|
||||
return `
|
||||
<div class="extracted-list-item" style="${isSubcat ? 'padding-left: 28px;' : ''}" data-parent="${cat.parentCategoryName || ''}">
|
||||
<input type="checkbox" checked data-index="${i}">
|
||||
${isSubcat ? '<span style="color: var(--gray-400); margin-right: 4px; font-size: 12px;">└</span>' : ''}
|
||||
<span class="item-text">
|
||||
<input type="text" value="${cat.name}" data-index="${i}">
|
||||
</span>
|
||||
|
|
@ -2305,13 +2305,23 @@
|
|||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function showCategoriesStep() {
|
||||
updateProgress(3);
|
||||
const categories = config.extractedData.categories || [];
|
||||
const topLevel = categories.filter(c => !c.parentCategoryName);
|
||||
const subcats = categories.filter(c => c.parentCategoryName);
|
||||
|
||||
let label = `${topLevel.length} menu categories`;
|
||||
if (subcats.length > 0) label += ` (${subcats.length} subcategories)`;
|
||||
|
||||
addMessage('ai', `
|
||||
<p>I found <strong>${categories.length} menu categories</strong>:</p>
|
||||
<p>I found <strong>${label}</strong>:</p>
|
||||
<div class="extracted-list" id="categoriesList">
|
||||
${categoriesHtml}
|
||||
${renderCategoryListHtml(categories)}
|
||||
</div>
|
||||
<div class="add-row">
|
||||
<input type="text" id="newCategoryName" placeholder="Add new category...">
|
||||
|
|
@ -2330,24 +2340,16 @@
|
|||
}
|
||||
|
||||
function removeCategory(index) {
|
||||
const removed = config.extractedData.categories[index];
|
||||
config.extractedData.categories.splice(index, 1);
|
||||
// Rebuild the list
|
||||
const list = document.getElementById('categoriesList');
|
||||
const categories = config.extractedData.categories;
|
||||
list.innerHTML = categories.map((cat, i) => `
|
||||
<div class="extracted-list-item">
|
||||
<input type="checkbox" checked data-index="${i}">
|
||||
<span class="item-text">
|
||||
<input type="text" value="${cat.name}" data-index="${i}">
|
||||
</span>
|
||||
<span class="item-count">${cat.itemCount || 0} items</span>
|
||||
<button class="remove-item" onclick="removeCategory(${i})">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
// If removing a parent, also remove its subcategories
|
||||
if (removed && !removed.parentCategoryName) {
|
||||
config.extractedData.categories = config.extractedData.categories.filter(
|
||||
c => c.parentCategoryName !== removed.name
|
||||
);
|
||||
}
|
||||
document.getElementById('categoriesList').innerHTML =
|
||||
renderCategoryListHtml(config.extractedData.categories);
|
||||
}
|
||||
|
||||
function addCategory() {
|
||||
|
|
@ -2358,26 +2360,8 @@
|
|||
config.extractedData.categories.push({ name, itemCount: 0 });
|
||||
input.value = '';
|
||||
|
||||
// Rebuild list
|
||||
removeCategory(-1); // Hacky way to rebuild without removing
|
||||
config.extractedData.categories.pop(); // Undo the splice
|
||||
|
||||
const list = document.getElementById('categoriesList');
|
||||
const categories = config.extractedData.categories;
|
||||
list.innerHTML = categories.map((cat, i) => `
|
||||
<div class="extracted-list-item">
|
||||
<input type="checkbox" checked data-index="${i}">
|
||||
<span class="item-text">
|
||||
<input type="text" value="${cat.name}" data-index="${i}">
|
||||
</span>
|
||||
<span class="item-count">${cat.itemCount || 0} items</span>
|
||||
<button class="remove-item" onclick="removeCategory(${i})">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`).join('');
|
||||
document.getElementById('categoriesList').innerHTML =
|
||||
renderCategoryListHtml(config.extractedData.categories);
|
||||
}
|
||||
|
||||
function confirmCategories() {
|
||||
|
|
@ -2390,10 +2374,16 @@
|
|||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||||
const nameInput = item.querySelector('.item-text input');
|
||||
if (checkbox.checked) {
|
||||
updatedCategories.push({
|
||||
const origCat = config.extractedData.categories[checkbox.dataset.index];
|
||||
const catObj = {
|
||||
name: nameInput.value,
|
||||
itemCount: config.extractedData.categories[checkbox.dataset.index]?.itemCount || 0
|
||||
});
|
||||
itemCount: origCat?.itemCount || 0
|
||||
};
|
||||
// Preserve parentCategoryName for subcategories
|
||||
if (origCat?.parentCategoryName) {
|
||||
catObj.parentCategoryName = origCat.parentCategoryName;
|
||||
}
|
||||
updatedCategories.push(catObj);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -2782,10 +2772,19 @@
|
|||
return;
|
||||
}
|
||||
|
||||
// Group items by category
|
||||
// Group items by category (subcategories grouped under parents)
|
||||
let itemsByCategory = {};
|
||||
let assignedItemIds = new Set();
|
||||
|
||||
// Build parent-to-subcategory map
|
||||
const subcatMap = {};
|
||||
categories.forEach(cat => {
|
||||
if (cat.parentCategoryName) {
|
||||
if (!subcatMap[cat.parentCategoryName]) subcatMap[cat.parentCategoryName] = [];
|
||||
subcatMap[cat.parentCategoryName].push(cat.name);
|
||||
}
|
||||
});
|
||||
|
||||
categories.forEach(cat => {
|
||||
const catItems = items.filter(item => item.category === cat.name);
|
||||
if (catItems.length > 0) {
|
||||
|
|
@ -2808,12 +2807,19 @@
|
|||
}
|
||||
|
||||
let itemsHtml = '';
|
||||
for (const [catName, catItems] of Object.entries(itemsByCategory)) {
|
||||
if (catItems.length === 0) continue;
|
||||
// Render top-level categories first, then their subcategories
|
||||
const topLevelCats = categories.filter(c => !c.parentCategoryName);
|
||||
const renderedCats = new Set();
|
||||
|
||||
itemsHtml += `
|
||||
<div style="margin-bottom: 16px;">
|
||||
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${catName} (${catItems.length})</h4>
|
||||
function renderCategoryItems(catName, isSubcat) {
|
||||
const catItems = itemsByCategory[catName];
|
||||
if (!catItems || catItems.length === 0) return '';
|
||||
renderedCats.add(catName);
|
||||
const indent = isSubcat ? 'margin-left: 20px;' : '';
|
||||
const prefix = isSubcat ? '<span style="color: var(--gray-400); margin-right: 4px;">└</span>' : '';
|
||||
return `
|
||||
<div style="margin-bottom: 16px; ${indent}">
|
||||
<h4 style="margin-bottom: 8px; color: var(--gray-700);">${prefix}${catName} (${catItems.length})</h4>
|
||||
<table class="items-table">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -2841,8 +2847,22 @@
|
|||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Render each top-level category and its subcategories
|
||||
topLevelCats.forEach(cat => {
|
||||
itemsHtml += renderCategoryItems(cat.name, false);
|
||||
const subs = subcatMap[cat.name] || [];
|
||||
subs.forEach(subName => {
|
||||
itemsHtml += renderCategoryItems(subName, true);
|
||||
});
|
||||
});
|
||||
|
||||
// Render any remaining categories not yet rendered
|
||||
for (const [catName, catItems] of Object.entries(itemsByCategory)) {
|
||||
if (renderedCats.has(catName) || catItems.length === 0) continue;
|
||||
itemsHtml += renderCategoryItems(catName, false);
|
||||
}
|
||||
|
||||
addMessage('ai', `
|
||||
|
|
@ -2894,7 +2914,11 @@
|
|||
|
||||
const { business, categories, modifiers, items } = config.extractedData;
|
||||
|
||||
document.getElementById('summaryCategories').textContent = categories.length;
|
||||
const topCats = categories.filter(c => !c.parentCategoryName);
|
||||
const subCats = categories.filter(c => c.parentCategoryName);
|
||||
document.getElementById('summaryCategories').textContent = subCats.length > 0
|
||||
? `${topCats.length} (${subCats.length} subcategories)`
|
||||
: categories.length;
|
||||
document.getElementById('summaryModifiers').textContent = modifiers.length;
|
||||
document.getElementById('summaryItems').textContent = items.length;
|
||||
|
||||
|
|
@ -2944,7 +2968,7 @@
|
|||
? `<p>Adding to: <strong>${config.menuName}</strong></p>`
|
||||
: `<p><strong>${business.name || 'Your Restaurant'}</strong></p>`}
|
||||
<ul style="margin: 12px 0; padding-left: 20px; color: var(--gray-600);">
|
||||
<li>${categories.length} categories</li>
|
||||
<li>${topCats.length} categories${subCats.length > 0 ? ` (${subCats.length} subcategories)` : ''}</li>
|
||||
<li>${modifiers.length} modifier templates</li>
|
||||
<li>${items.length} menu items</li>
|
||||
${imagesSummary}
|
||||
|
|
@ -3204,13 +3228,26 @@
|
|||
</div>
|
||||
`;
|
||||
|
||||
// Build subcategory lookup for preview
|
||||
const previewSubcatMap = {};
|
||||
categories.forEach(cat => {
|
||||
if (cat.parentCategoryName) {
|
||||
if (!previewSubcatMap[cat.parentCategoryName]) previewSubcatMap[cat.parentCategoryName] = [];
|
||||
previewSubcatMap[cat.parentCategoryName].push(cat);
|
||||
}
|
||||
});
|
||||
const previewRendered = new Set();
|
||||
|
||||
function renderPreviewCategory(cat, isSubcat) {
|
||||
previewRendered.add(cat.name);
|
||||
const catItems = items.filter(item => item.category === cat.name);
|
||||
previewHtml += `
|
||||
<div class="category">
|
||||
<h2 onclick="this.parentElement.classList.toggle('collapsed')">
|
||||
const tag = isSubcat ? 'h3' : 'h2';
|
||||
const indent = isSubcat ? 'margin-left: 20px;' : '';
|
||||
let html = `
|
||||
<div class="category" style="${indent}">
|
||||
<${tag} onclick="this.parentElement.classList.toggle('collapsed')" ${isSubcat ? 'style="font-size: 16px;"' : ''}>
|
||||
${cat.name} <span class="toggle-icon">[+/-]</span>
|
||||
</h2>
|
||||
</${tag}>
|
||||
<div class="category-items">
|
||||
${catItems.map(item => `
|
||||
<div class="item">
|
||||
|
|
@ -3226,10 +3263,26 @@
|
|||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
${catItems.length === 0 ? '<div class="item" style="color:#999;">No items in this category</div>' : ''}
|
||||
${catItems.length === 0 && !(previewSubcatMap[cat.name]?.length) ? '<div class="item" style="color:#999;">No items in this category</div>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
// Render subcategories inline
|
||||
if (previewSubcatMap[cat.name]) {
|
||||
previewSubcatMap[cat.name].forEach(sub => {
|
||||
html += renderPreviewCategory(sub, true);
|
||||
});
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
categories.filter(c => !c.parentCategoryName).forEach(cat => {
|
||||
previewHtml += renderPreviewCategory(cat, false);
|
||||
});
|
||||
// Render any unrendered categories
|
||||
categories.forEach(cat => {
|
||||
if (!previewRendered.has(cat.name)) {
|
||||
previewHtml += renderPreviewCategory(cat, false);
|
||||
}
|
||||
});
|
||||
|
||||
previewHtml += `
|
||||
|
|
|
|||
Reference in a new issue