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:
parent
715d947e4b
commit
a181c1b90a
1 changed files with 195 additions and 12 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
Reference in a new issue