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:
@@ -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.'
|
||||
}
|
||||
|
||||
@@ -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[],
|
||||
|
||||
Reference in New Issue
Block a user