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
|
// Hide upload section, show conversation
|
||||||
document.getElementById('uploadSection').style.display = 'none';
|
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', `
|
addMessage('ai', `
|
||||||
<div style="display:flex;align-items:center;gap:12px;">
|
<div style="display:flex;align-items:center;gap:12px;">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
|
|
@ -1422,29 +1616,18 @@
|
||||||
throw new Error(result.MESSAGE || 'Failed to analyze URL');
|
throw new Error(result.MESSAGE || 'Failed to analyze URL');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store extracted data
|
|
||||||
config.extractedData = result.DATA;
|
config.extractedData = result.DATA;
|
||||||
config.sourceUrl = result.sourceUrl;
|
config.sourceUrl = result.sourceUrl;
|
||||||
|
|
||||||
// Store image mappings for matching uploaded images to items
|
|
||||||
config.imageMappings = result.DATA.imageMappings || [];
|
config.imageMappings = result.DATA.imageMappings || [];
|
||||||
|
|
||||||
// Log debug info
|
|
||||||
console.log('=== URL IMPORT RESPONSE ===');
|
console.log('=== URL IMPORT RESPONSE ===');
|
||||||
console.log('Source URL:', result.sourceUrl);
|
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);
|
console.log('Extracted data:', result.DATA);
|
||||||
if (result.steps) {
|
if (result.steps) console.log('Steps:', result.steps);
|
||||||
console.log('Steps:', result.steps);
|
|
||||||
}
|
|
||||||
console.log('===========================');
|
console.log('===========================');
|
||||||
|
|
||||||
// Remove loading message and start conversation flow
|
|
||||||
document.getElementById('conversation').innerHTML = '';
|
document.getElementById('conversation').innerHTML = '';
|
||||||
|
|
||||||
// In add-menu mode, skip business info and header
|
|
||||||
if (config.businessId && config.menuId) {
|
if (config.businessId && config.menuId) {
|
||||||
showCategoriesStep();
|
showCategoriesStep();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Reference in a new issue