Fix wizard flow and add detailed modifier view

Major Changes:
1. Fixed infinite loop in wizard flow - uncertain modifiers step now correctly advances to final review instead of looping back to items
2. Moved uncertain modifier assignment to AFTER items review (makes more sense for user to see items first)
3. Added detailed modifier visualization on uncertain modifiers step showing:
   - Source image indicator (which image the modifier was extracted from)
   - Full list of all options with prices
   - Required/optional status
   - Option count summary

Technical Details:
- Backend: Added sourceImageIndex tracking in analyzeMenuImages.cfm to record which image each modifier came from
- Frontend: Enhanced uncertain modifiers step with inline detailed view showing complete modifier structure
- Flow correction: showUncertainModifiersStep() now calls showFinalStep() instead of showItemsStep() to prevent loop
- Improved error handling in API calls with detailed error messages from Claude API

Flow Changes:
- Old: Upload → Business → Categories → Modifiers → Uncertain Modifiers → Items → [LOOP]
- New: Upload → Business → Categories → Modifiers → Items → Uncertain Modifiers → Final Review

Model Configuration:
- Using claude-sonnet-4-20250514 for menu image analysis
- Added better error reporting to surface API issues (auth, credits, etc.)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
John Mizerek 2026-01-15 16:53:09 -08:00
parent 8999805ec6
commit fe383f40d0
3 changed files with 78 additions and 22 deletions

View file

@ -141,7 +141,19 @@
</cfif> </cfif>
<cfif httpStatusCode NEQ 200> <cfif httpStatusCode NEQ 200>
<cfthrow message="Claude API error on image #imgIndex#: #httpResult.statusCode#" detail="#httpResult.fileContent#"> <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 on image #imgIndex#: #httpResult.statusCode# - #errorDetail#">
</cfif> </cfif>
<!--- Parse response ---> <!--- Parse response --->

View file

@ -218,7 +218,7 @@
</select> </select>
</div> </div>
<button type="submit" class="login-btn" id="continueBtn">Continue to Portal</button> <button type="submit" class="login-btn" id="continueBtn">Continue</button>
</form> </form>
</div> </div>
</div> </div>
@ -311,16 +311,12 @@
// Load user's businesses // Load user's businesses
await this.loadBusinesses(); await this.loadBusinesses();
if (this.businesses.length === 1) { if (this.businesses.length === 0) {
// Auto-select if only one business // No businesses - go directly to wizard
this.selectBusinessById(this.businesses[0].BusinessID); this.startNewRestaurant();
} else if (this.businesses.length > 1) {
// Show business selection
this.showStep('business');
} else { } else {
// No businesses - show error // Show business selection (even if just one, so they can access wizard)
errorEl.textContent = 'No businesses associated with this account.'; this.showStep('business');
errorEl.classList.add('show');
} }
} else { } else {
errorEl.textContent = data.ERROR || data.MESSAGE || 'Invalid credentials'; errorEl.textContent = data.ERROR || data.MESSAGE || 'Invalid credentials';
@ -374,14 +370,31 @@
populateBusinessSelect() { populateBusinessSelect() {
const select = document.getElementById('businessSelect'); const select = document.getElementById('businessSelect');
select.innerHTML = '<option value="">Choose a business...</option>';
this.businesses.forEach(biz => { if (this.businesses.length === 0) {
const option = document.createElement('option'); select.innerHTML = '<option value="">No businesses yet - use wizard below</option>';
option.value = biz.BusinessID; select.disabled = true;
option.textContent = biz.BusinessName; document.getElementById('continueBtn').disabled = true;
select.appendChild(option); } else {
}); select.innerHTML = '<option value="">Choose a business...</option>';
select.disabled = false;
document.getElementById('continueBtn').disabled = false;
this.businesses.forEach(biz => {
const option = document.createElement('option');
option.value = biz.BusinessID;
option.textContent = biz.BusinessName;
select.appendChild(option);
});
// Add "New Business Wizard" option at the end
const wizardOption = document.createElement('option');
wizardOption.value = 'NEW_WIZARD';
wizardOption.textContent = '✨ New Business Wizard';
wizardOption.style.fontWeight = 'bold';
wizardOption.style.color = 'var(--primary)';
select.appendChild(wizardOption);
}
}, },
showStep(step) { showStep(step) {
@ -391,7 +404,9 @@
selectBusiness() { selectBusiness() {
const businessId = document.getElementById('businessSelect').value; const businessId = document.getElementById('businessSelect').value;
if (businessId) { if (businessId === 'NEW_WIZARD') {
this.startNewRestaurant();
} else if (businessId) {
this.selectBusinessById(businessId); this.selectBusinessById(businessId);
} }
}, },
@ -401,6 +416,13 @@
window.location.href = BASE_PATH + `/portal/index.html?bid=${businessId}`; window.location.href = BASE_PATH + `/portal/index.html?bid=${businessId}`;
}, },
startNewRestaurant() {
// Clear any existing business selection
localStorage.removeItem('payfrit_portal_business');
// Redirect to wizard without businessId
window.location.href = BASE_PATH + `/portal/setup-wizard.html`;
},
logout() { logout() {
localStorage.removeItem('payfrit_portal_token'); localStorage.removeItem('payfrit_portal_token');
localStorage.removeItem('payfrit_portal_userid'); localStorage.removeItem('payfrit_portal_userid');

View file

@ -1624,8 +1624,8 @@
); );
if (uncertainModifiers.length === 0 || categories.length === 0) { if (uncertainModifiers.length === 0 || categories.length === 0) {
// No uncertain modifiers or no categories, skip to items // No uncertain modifiers or no categories, skip to final review
showItemsStep(); showFinalStep();
return; return;
} }
@ -1640,12 +1640,22 @@
if (currentIndex >= uncertainModifiers.length) { if (currentIndex >= uncertainModifiers.length) {
// All uncertain modifiers have been processed, apply assignments and continue // All uncertain modifiers have been processed, apply assignments and continue
applyUncertainModifierAssignments(); applyUncertainModifierAssignments();
showItemsStep(); showFinalStep();
return; return;
} }
const modifier = uncertainModifiers[currentIndex]; const modifier = uncertainModifiers[currentIndex];
// Build detailed modifier view
const sourceImg = modifier.sourceImageIndex ? `Image ${modifier.sourceImageIndex}` : 'Unknown source';
const optionsCount = (modifier.options || []).length;
const optionsList = (modifier.options || []).filter(opt => opt && opt.name).map(opt => `
<div class="modifier-option-detail">
<span class="option-name">${opt.name}</span>
<span class="option-price">${opt.price ? `+$${opt.price.toFixed(2)}` : '$0.00'}</span>
</div>
`).join('');
// Ask user about this modifier // Ask user about this modifier
const categoryOptions = categories.map((cat, i) => ` const categoryOptions = categories.map((cat, i) => `
<label class="category-option"> <label class="category-option">
@ -1656,6 +1666,18 @@
addMessage('ai', ` addMessage('ai', `
<p>I found the modifier template <strong>"${modifier.name}"</strong> but I'm not sure which items it applies to.</p> <p>I found the modifier template <strong>"${modifier.name}"</strong> but I'm not sure which items it applies to.</p>
<div class="modifier-details-view" style="background: var(--gray-50); border: 1px solid var(--gray-200); border-radius: 8px; padding: 16px; margin: 16px 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span style="font-weight: 500;">${modifier.name}</span>
<span class="source-badge">${sourceImg}</span>
</div>
<div style="color: var(--gray-600); font-size: 14px; margin-bottom: 8px;">
<strong>${optionsCount} option${optionsCount !== 1 ? 's' : ''}</strong> • ${modifier.required ? 'Required' : 'Optional'}
</div>
<div class="modifier-options-list">
${optionsList}
</div>
</div>
<p>Select the categories where this modifier should be applied, or skip if it doesn't apply automatically:</p> <p>Select the categories where this modifier should be applied, or skip if it doesn't apply automatically:</p>
<div class="category-selection" style="display: flex; flex-direction: column; gap: 8px; margin: 16px 0;"> <div class="category-selection" style="display: flex; flex-direction: column; gap: 8px; margin: 16px 0;">
${categoryOptions} ${categoryOptions}