Add My Bar, Bartender, Recommend features + drink images

- Drink Images: upload/display photos of bottles/cans on drink cards and detail pages
- My Bar: inventory tracker for spirits, liqueurs, mixers, bitters, garnishes, tools
- Bartender: AI-powered cocktail recipe generation, "what can I make" suggestions,
  saved recipes. Cross-references bar inventory for ingredient availability.
- Recommend: AI flavor profile analysis, personalized drink recommendations,
  "find similar" drinks based on highly-rated favorites
- Navigation: desktop sidebar with all 8 routes, mobile bottom nav with
  4 primary items + "More" popup menu
- New Prisma models: BarItem, Recipe, FlavorProfile
- Backup/restore updated to include bar items

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JP Scott
2026-03-01 18:28:02 -07:00
parent d8f069cce4
commit 2ac2c4b2d4
40 changed files with 3709 additions and 11 deletions

View File

@@ -166,3 +166,119 @@ Do not include any text before or after the JSON array. Example format:
}
]`
}
export const COCKTAIL_RECIPE_PROMPT = `You are an expert bartender and cocktail specialist. Generate a detailed cocktail recipe.
The user's home bar inventory is provided. For each ingredient, indicate whether the user has it.
## User's Bar Inventory
{barInventory}
## Instructions
Create a recipe for the requested cocktail. Return a valid JSON object:
- "title" (string): Cocktail name
- "ingredients" (array): Each { "name": string, "amount": string, "available": boolean }
- "steps" (string array): Step-by-step instructions
- "garnish" (string, optional): Garnish description
- "glassware" (string, optional): Recommended glass
- "notes" (string, optional): Tips, variations, or history
Mark ingredients available:true only if matching item exists in bar inventory. Do not include text before or after the JSON.`
export const WHAT_CAN_I_MAKE_PROMPT = `You are an expert bartender. Based on the user's bar inventory, suggest cocktails they can make.
## User's Bar Inventory
{barInventory}
## Instructions
Suggest cocktails prioritizing those where ALL ingredients are available, then those missing 1-2 ingredients.
Return a valid JSON array of objects:
- "title" (string): Cocktail name
- "ingredients" (array): Each { "name": string, "amount": string, "available": boolean }
- "steps" (string array): Brief preparation steps
- "garnish" (string, optional)
- "glassware" (string, optional)
- "missingCount" (number): How many ingredients missing (0 = can make now)
Sort by missingCount ascending. Return up to 10. Do not include text before or after the JSON.`
export function buildBarInventoryString(items: { name: string; category: string; quantity: string }[]): string {
const byCategory: Record<string, string[]> = {}
for (const item of items) {
if (item.quantity === "EMPTY") continue
if (!byCategory[item.category]) byCategory[item.category] = []
byCategory[item.category].push(`${item.name} (${item.quantity.toLowerCase()})`)
}
return Object.entries(byCategory)
.map(([cat, items]) => `### ${cat}\n${items.map(i => `- ${i}`).join('\n')}`)
.join('\n\n') || 'No items in bar inventory.'
}
export const FLAVOR_PROFILE_PROMPT = `You are an expert sommelier and drink taste profiler. Analyze the user's drink history and ratings to build a flavor profile.
## User's Rated Drinks
{drinkHistory}
## Instructions
Create a comprehensive flavor profile. Return a valid JSON object:
- "summary" (string): 2-3 sentence natural language summary
- "topFlavors" (string array): Top 5-8 flavor descriptors they prefer
- "avoidFlavors" (string array): Flavors they tend to dislike
- "preferredTypes" (string array): Preferred drink types/styles
- "adventureScore" (number, 0-1): How adventurous/varied their choices are
Do not include text before or after the JSON.`
export const RECOMMEND_DRINK_PROMPT = `You are a drink recommendation expert. Based on the user's flavor profile and context, suggest drinks they'd enjoy.
## User's Flavor Profile
{flavorProfile}
## Additional Context
{context}
## Instructions
Recommend 3-5 drinks. Return a valid JSON array of objects:
- "name" (string): Specific drink name
- "type" (string): BEER, WINE, COCKTAIL, SPIRIT, or OTHER
- "subType" (string, optional): Style
- "brewery" (string, optional): Producer
- "reason" (string): 1-2 sentence personalized explanation
- "matchScore" (number, 0-1): How well it matches their profile
Sort by matchScore descending. Do not include text before or after the JSON.`
export const SIMILAR_DRINK_PROMPT = `You are a drink expert. The user loves a specific drink and wants similar ones.
## Drink They Love
{sourceDrink}
## User's Flavor Profile
{flavorProfile}
## Instructions
Suggest 3-5 similar drinks. Return a valid JSON array of objects:
- "name" (string): Specific drink name
- "type" (string): BEER, WINE, COCKTAIL, SPIRIT, or OTHER
- "subType" (string, optional): Style
- "brewery" (string, optional): Producer
- "reason" (string): Why it's similar and why they'd like it
- "similarity" (number, 0-1): How similar to the source drink
Sort by similarity descending. Do not include text before or after the JSON.`
export function buildDrinkHistoryString(drinks: { name: string; type: string; subType?: string | null; brewery?: string | null; avgRating: number | null; ratingCount: number; wouldReorder: boolean }[]): string {
return drinks
.filter(d => d.avgRating !== null)
.sort((a, b) => (b.avgRating ?? 0) - (a.avgRating ?? 0))
.map(d => {
const parts = [`${d.name} (${d.type})`]
if (d.subType) parts.push(`Style: ${d.subType}`)
if (d.brewery) parts.push(`From: ${d.brewery}`)
parts.push(`Rating: ${d.avgRating}/5 (${d.ratingCount} rating${d.ratingCount !== 1 ? 's' : ''})`)
if (d.wouldReorder) parts.push('Would reorder: Yes')
return `- ${parts.join(' | ')}`
})
.join('\n') || 'No rated drinks yet.'
}

View File

@@ -42,6 +42,8 @@ export interface DrinkSearchResult {
export interface AIProvider {
name: string
sendTextRequest(systemPrompt: string, userMessage: string): Promise<string>
sendVisionRequest(systemPrompt: string, imageBase64: string, mimeType: string): Promise<string>
extractMenuItems(imageBase64: string, mimeType: string): Promise<MenuExtractionResult>
recommendDrinks(
extractedItems: ExtractedMenuItem[],