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:
93
src/app/api/bartender/recreate/route.ts
Normal file
93
src/app/api/bartender/recreate/route.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
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 { COCKTAIL_RECIPE_PROMPT, buildBarInventoryString } from "@/lib/ai/prompts"
|
||||
import { z } from "zod"
|
||||
|
||||
const recreateSchema = z.object({
|
||||
cocktailName: z.string().min(1).max(200),
|
||||
drinkId: z.string().optional(),
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const session = await auth()
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||
}
|
||||
|
||||
const { success: withinLimit } = rateLimit(`bartender-recreate:${session.user.id}`, 10, 60000)
|
||||
if (!withinLimit) {
|
||||
return NextResponse.json(
|
||||
{ error: "Too many requests. Please wait a moment." },
|
||||
{ status: 429 }
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const parsed = recreateSchema.safeParse(body)
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Invalid request" }, { status: 400 })
|
||||
}
|
||||
|
||||
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 },
|
||||
})
|
||||
|
||||
const inventoryString = buildBarInventoryString(barItems)
|
||||
const prompt = COCKTAIL_RECIPE_PROMPT.replace("{barInventory}", inventoryString)
|
||||
|
||||
const apiKey = decrypt(apiKeyRecord.encryptedKey, apiKeyRecord.iv)
|
||||
const provider = createProvider(apiKeyRecord.provider, apiKey)
|
||||
|
||||
const rawResponse = await provider.sendTextRequest(
|
||||
prompt,
|
||||
`Generate a recipe for: ${parsed.data.cocktailName}`
|
||||
)
|
||||
|
||||
// Parse JSON from response
|
||||
let recipe
|
||||
try {
|
||||
recipe = JSON.parse(rawResponse)
|
||||
} catch {
|
||||
// Try to extract from markdown code blocks or find JSON object
|
||||
const codeBlockMatch = rawResponse.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/)
|
||||
if (codeBlockMatch) {
|
||||
recipe = JSON.parse(codeBlockMatch[1].trim())
|
||||
} else {
|
||||
const objectMatch = rawResponse.match(/\{[\s\S]*\}/)
|
||||
if (objectMatch) {
|
||||
recipe = JSON.parse(objectMatch[0])
|
||||
} else {
|
||||
throw new Error("Could not parse recipe from AI response")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(recipe)
|
||||
} catch (error) {
|
||||
console.error("Bartender recreate error:", error)
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate recipe. Please try again." },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user