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:
91
src/app/api/bartender/suggest/route.ts
Normal file
91
src/app/api/bartender/suggest/route.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { NextResponse } from "next/server"
|
||||
import { auth } from "@/lib/auth"
|
||||
import { prisma } from "@/lib/prisma"
|
||||
import { decrypt } from "@/lib/encryption"
|
||||
import { createProvider } from "@/lib/ai/provider-factory"
|
||||
import { rateLimit } from "@/lib/rate-limit"
|
||||
import { WHAT_CAN_I_MAKE_PROMPT, buildBarInventoryString } from "@/lib/ai/prompts"
|
||||
|
||||
export async function POST() {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { success: withinLimit } = rateLimit(`bartender-suggest:${session.user.id}`, 5, 60000)
|
||||
if (!withinLimit) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests. Please wait a moment." },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKeyRecord = await prisma.userApiKey.findFirst({
|
||||
where: { userId: session.user.id, isActive: true },
|
||||
})
|
||||
|
||||
if (!apiKeyRecord) {
|
||||
return NextResponse.json(
|
||||
{ error: "No AI provider configured. Add an API key in Settings." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const barItems = await prisma.barItem.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
quantity: { not: "EMPTY" },
|
||||
},
|
||||
select: { name: true, category: true, quantity: true },
|
||||
})
|
||||
|
||||
if (barItems.length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "No items in your bar inventory. Add items to your bar first." },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const inventoryString = buildBarInventoryString(barItems)
|
||||
const prompt = WHAT_CAN_I_MAKE_PROMPT.replace("{barInventory}", inventoryString)
|
||||
|
||||
const apiKey = decrypt(apiKeyRecord.encryptedKey, apiKeyRecord.iv)
|
||||
const provider = createProvider(apiKeyRecord.provider, apiKey)
|
||||
|
||||
const rawResponse = await provider.sendTextRequest(
|
||||
prompt,
|
||||
"What cocktails can I make with my bar inventory?"
|
||||
)
|
||||
|
||||
// Parse JSON from response
|
||||
let suggestions
|
||||
try {
|
||||
suggestions = JSON.parse(rawResponse)
|
||||
} catch {
|
||||
const codeBlockMatch = rawResponse.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/)
|
||||
if (codeBlockMatch) {
|
||||
suggestions = JSON.parse(codeBlockMatch[1].trim())
|
||||
} else {
|
||||
const arrayMatch = rawResponse.match(/\[[\s\S]*\]/)
|
||||
if (arrayMatch) {
|
||||
suggestions = JSON.parse(arrayMatch[0])
|
||||
} else {
|
||||
throw new Error("Could not parse suggestions from AI response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Array.isArray(suggestions)) {
|
||||
suggestions = []
|
||||
}
|
||||
|
||||
return NextResponse.json({ suggestions })
|
||||
} catch (error) {
|
||||
console.error("Bartender suggest error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate suggestions. Please try again." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user