Setup wizard: interactive menu discovery before extraction

- Phase 1: Quick scan shows detected menu pages with checkboxes
- User confirms which pages are actual menus
- Phase 2: Each page extracted individually through Claude
- Shows progress for each page being processed
- Falls back to single-page extract if no sub-pages found
- Optional extra URL field for manual addition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-03-14 17:03:58 -07:00
parent 715d947e4b
commit a181c1b90a

View file

@ -1401,6 +1401,200 @@
// 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>Scanning website for menu pages...</span>
</div>
<p style="font-size:13px;color:var(--gray-500);margin-top:8px;">This takes about 30 seconds while I crawl the site and look for menu sub-pages.</p>
`);
try {
// Phase 1: Discovery — quick Playwright crawl to find menu pages
const discoverResp = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'discover', url })
});
const discoverResult = await discoverResp.json();
if (!discoverResult.OK) {
throw new Error(discoverResult.MESSAGE || 'Failed to scan website');
}
console.log('=== DISCOVERY RESULT ===', discoverResult);
document.getElementById('conversation').innerHTML = '';
const menuPages = discoverResult.menuPages || [];
const siteName = discoverResult.siteName || '';
if (menuPages.length > 1) {
// Multiple menus found — show confirmation step
const pageListHtml = menuPages.map((p, i) => `
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--gray-50);border-radius:8px;margin-bottom:6px;cursor:pointer;">
<input type="checkbox" checked data-menu-url="${p.url}" data-menu-name="${p.name}" style="width:18px;height:18px;">
<span style="font-weight:500;">${p.name}</span>
<span style="font-size:12px;color:var(--gray-400);margin-left:auto;">${p.url}</span>
</label>
`).join('');
addMessage('ai', `
<p>I found <strong>${menuPages.length} menu pages</strong>${siteName ? ' for <strong>' + siteName + '</strong>' : ''}:</p>
<div id="discoveredMenuPages" style="margin:12px 0;">
${pageListHtml}
</div>
<p style="font-size:13px;color:var(--gray-500);">Uncheck any that aren't actual menus. Each checked page will be analyzed separately.</p>
<div style="margin-top:8px;">
<label style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--gray-50);border-radius:8px;margin-bottom:6px;">
<span style="font-size:13px;">Additional menu URL (optional):</span>
<input type="text" id="extraMenuUrl" placeholder="https://..." style="flex:1;padding:4px 8px;border:1px solid var(--gray-200);border-radius:4px;font-size:13px;">
</label>
</div>
<div class="action-buttons" style="margin-top:12px;">
<button class="btn btn-primary" onclick="extractConfirmedMenuPages('${url}')">Extract Menus</button>
<button class="btn btn-secondary" onclick="skipDiscoveryExtractAll('${url}')">Extract as Single Page</button>
</div>
`);
} else {
// No sub-pages or just one — fall through to single-page extract
await extractSingleUrl(url);
}
} 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>
`);
}
}
// Phase 2: Extract each confirmed menu page individually
async function extractConfirmedMenuPages(mainUrl) {
const checkboxes = document.querySelectorAll('#discoveredMenuPages input[type="checkbox"]:checked');
const pages = Array.from(checkboxes).map(cb => ({
url: cb.dataset.menuUrl,
name: cb.dataset.menuName
}));
// Add extra URL if provided
const extraUrl = (document.getElementById('extraMenuUrl')?.value || '').trim();
if (extraUrl) {
const slug = extraUrl.replace(/.*\//, '').replace(/[-_]/g, ' ');
pages.push({ url: extraUrl, name: slug ? slug.charAt(0).toUpperCase() + slug.slice(1) : 'Extra' });
}
if (pages.length === 0) {
alert('Please select at least one menu page.');
return;
}
document.getElementById('conversation').innerHTML = '';
// Combined results
const allItems = [];
const allCategories = [];
const allMenus = [];
let businessInfo = {};
let totalProcessed = 0;
for (const page of pages) {
addMessage('ai', `
<div style="display:flex;align-items:center;gap:12px;">
<div class="loading-spinner"></div>
<span>Extracting <strong>${page.name}</strong> menu... (${totalProcessed + 1} of ${pages.length})</span>
</div>
`);
try {
const resp = await fetch(`${config.apiBaseUrl}/setup/analyzeMenuUrl.php`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'extract_page', url: page.url, menuName: page.name })
});
const result = await resp.json();
if (result.OK && result.DATA) {
const data = result.DATA;
// Merge business info (first non-empty wins)
if (data.business && Object.keys(data.business).length > Object.keys(businessInfo).length) {
businessInfo = { ...businessInfo, ...data.business };
}
// Add categories with menu tag
(data.categories || []).forEach(cat => {
const catName = typeof cat === 'string' ? cat : cat.name;
if (catName && !allCategories.find(c => c.name === catName && c.menuName === page.name)) {
allCategories.push({ name: catName, menuName: page.name });
}
});
// Add items (already tagged with menu name by backend)
(data.items || []).forEach(item => allItems.push(item));
allMenus.push({ name: page.name });
totalProcessed++;
// Update with progress
document.getElementById('conversation').innerHTML = '';
addMessage('ai', `
<p>Extracted <strong>${page.name}</strong>: ${data.items?.length || 0} items in ${data.categories?.length || 0} categories</p>
`);
} else {
document.getElementById('conversation').innerHTML = '';
addMessage('ai', `<p style="color:var(--warning);">Could not extract items from ${page.name} page.</p>`);
totalProcessed++;
}
} catch (err) {
console.error(`Error extracting ${page.name}:`, err);
document.getElementById('conversation').innerHTML = '';
addMessage('ai', `<p style="color:var(--danger);">Error extracting ${page.name}: ${err.message}</p>`);
totalProcessed++;
}
}
// Build final combined data
document.getElementById('conversation').innerHTML = '';
config.extractedData = {
business: businessInfo,
categories: allCategories,
items: allItems,
modifiers: [],
menus: allMenus.length > 1 ? allMenus : [],
imageMappings: [],
imageUrls: [],
headerCandidateIndices: [],
};
config.sourceUrl = mainUrl;
config.imageMappings = [];
const totalItems = allItems.length;
const totalCats = allCategories.length;
addMessage('ai', `
<p>Done! Found <strong>${totalItems} items</strong> across <strong>${totalCats} categories</strong> in <strong>${allMenus.length} menus</strong>.</p>
`);
console.log('=== MULTI-PAGE EXTRACT RESULT ===', config.extractedData);
// Brief pause then continue to business info
await new Promise(r => setTimeout(r, 1500));
document.getElementById('conversation').innerHTML = '';
if (config.businessId && config.menuId) {
showCategoriesStep();
} else {
showBusinessInfoStep();
}
}
// Fallback: extract entire site as single page (original behavior)
async function skipDiscoveryExtractAll(url) {
document.getElementById('conversation').innerHTML = '';
await extractSingleUrl(url);
}
async function extractSingleUrl(url) {
addMessage('ai', `
<div style="display:flex;align-items:center;gap:12px;">
<div class="loading-spinner"></div>
@ -1422,29 +1616,18 @@
throw new Error(result.MESSAGE || 'Failed to analyze URL');
}
// Store extracted data
config.extractedData = result.DATA;
config.sourceUrl = result.sourceUrl;
// Store image mappings for matching uploaded images to items
config.imageMappings = result.DATA.imageMappings || [];
// 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('Image mappings:', config.imageMappings.length);
console.log('Extracted data:', result.DATA);
if (result.steps) {
console.log('Steps:', result.steps);
}
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 {