4a90ad910c
Add root Dockerfile (standalone Next output, multi-stage pnpm build), drop in-stack caddy in favor of publishing web's port for an external traefik LXC (file-provider dynamic config included), and document the portainer deploy flow. Also fixes issues that blocked any production build: a bad auth-client type cast, the ai SDK's mimeType->mediaType rename, an implicit-any callback param, and push.ts eagerly calling webpush.setVapidDetails at module import time (which crashed page-data collection whenever VAPID env vars weren't present at build) — now lazily configured on first send. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
61 lines
1.8 KiB
TypeScript
61 lines
1.8 KiB
TypeScript
import { generateObject } from "ai";
|
|
import { z } from "zod";
|
|
import { resolveModel, type AiConfig } from "../factory";
|
|
|
|
const ImportedRecipeSchema = z.object({
|
|
title: z.string(),
|
|
description: z.string().optional(),
|
|
baseServings: z.number().int().min(1).optional(),
|
|
prepMins: z.number().int().min(0).optional(),
|
|
cookMins: z.number().int().min(0).optional(),
|
|
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
|
|
dietaryTags: z.object({
|
|
vegan: z.boolean().optional(),
|
|
vegetarian: z.boolean().optional(),
|
|
glutenFree: z.boolean().optional(),
|
|
dairyFree: z.boolean().optional(),
|
|
nutFree: z.boolean().optional(),
|
|
halal: z.boolean().optional(),
|
|
kosher: z.boolean().optional(),
|
|
}).optional(),
|
|
ingredients: z.array(z.object({
|
|
rawName: z.string(),
|
|
quantity: z.string().optional(),
|
|
unit: z.string().optional(),
|
|
note: z.string().optional(),
|
|
})),
|
|
steps: z.array(z.object({
|
|
instruction: z.string(),
|
|
timerSeconds: z.number().int().optional(),
|
|
})),
|
|
});
|
|
|
|
export type ImportedRecipe = z.infer<typeof ImportedRecipeSchema>;
|
|
|
|
export async function importFromPhoto(
|
|
imageBase64: string,
|
|
mimeType: "image/jpeg" | "image/png" | "image/webp",
|
|
config?: AiConfig,
|
|
_locale?: string,
|
|
): Promise<ImportedRecipe> {
|
|
const model = resolveModel(config);
|
|
|
|
const { object } = await generateObject({
|
|
model,
|
|
schema: ImportedRecipeSchema,
|
|
system:
|
|
"You are a recipe extraction specialist. Extract the complete recipe from the provided image. Be precise with ingredient quantities and cooking instructions.",
|
|
messages: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "image", image: imageBase64, mediaType: mimeType },
|
|
{ type: "text", text: "Extract the complete recipe from this image." },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
return object;
|
|
}
|