feat(routines): add minoxidil beard/mustache option to routine suggestions

- Add include_minoxidil_beard flag to SuggestRoutineRequest and SuggestBatchRequest
- Detect minoxidil products by scanning name, brand, INCI and actives; pass them
  to the LLM even though they are medications
- Inject CELE UŻYTKOWNIKA context block into prompts when flag is enabled
- Add _build_objectives_context() returning empty string when flag is off
- Add call_gemini() helper that centralises Gemini API calls and logs every
  request/response to a new ai_call_logs table (AICallLog model + /ai-logs router)
- Nginx: raise client_max_body_size to 16 MB for photo uploads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Piotr Oleszczyk 2026-03-01 19:46:07 +01:00
parent 3aa03b412b
commit 75ef1bca56
15 changed files with 337 additions and 62 deletions

View file

@ -132,6 +132,8 @@
"suggest_contextLabel": "Additional context for AI",
"suggest_contextOptional": "(optional)",
"suggest_contextPlaceholder": "e.g. party night, focusing on hydration...",
"suggest_minoxidilToggleLabel": "Prioritize beard/mustache density (minoxidil)",
"suggest_minoxidilToggleHint": "When enabled, AI will explicitly consider minoxidil for beard/mustache areas if available.",
"suggest_generateBtn": "Generate suggestion",
"suggest_generating": "Generating…",
"suggest_proposalTitle": "Suggestion",

View file

@ -132,6 +132,8 @@
"suggest_contextLabel": "Dodatkowy kontekst dla AI",
"suggest_contextOptional": "(opcjonalny)",
"suggest_contextPlaceholder": "np. wieczór imprezowy, skupiam się na nawilżeniu...",
"suggest_minoxidilToggleLabel": "Priorytet: gęstość brody/wąsów (minoksydyl)",
"suggest_minoxidilToggleHint": "Po włączeniu AI jawnie uwzględni minoksydyl dla obszaru brody/wąsów, jeśli jest dostępny.",
"suggest_generateBtn": "Generuj propozycję",
"suggest_generating": "Generuję…",
"suggest_proposalTitle": "Propozycja",

View file

@ -141,12 +141,14 @@ export const suggestRoutine = (body: {
routine_date: string;
part_of_day: PartOfDay;
notes?: string;
include_minoxidil_beard?: boolean;
}): Promise<RoutineSuggestion> => api.post('/routines/suggest', body);
export const suggestBatch = (body: {
from_date: string;
to_date: string;
notes?: string;
include_minoxidil_beard?: boolean;
}): Promise<BatchSuggestion> => api.post('/routines/suggest-batch', body);
export const getGroomingSchedule = (): Promise<GroomingSchedule[]> =>

View file

@ -14,13 +14,19 @@ export const actions: Actions = {
const routine_date = form.get('routine_date') as string;
const part_of_day = form.get('part_of_day') as 'am' | 'pm';
const notes = (form.get('notes') as string) || undefined;
const include_minoxidil_beard = form.get('include_minoxidil_beard') === 'on';
if (!routine_date || !part_of_day) {
return fail(400, { error: 'Data i pora dnia są wymagane.' });
}
try {
const suggestion = await suggestRoutine({ routine_date, part_of_day, notes });
const suggestion = await suggestRoutine({
routine_date,
part_of_day,
notes,
include_minoxidil_beard
});
return { suggestion, routine_date, part_of_day };
} catch (e) {
return fail(502, { error: (e as Error).message });
@ -32,6 +38,7 @@ export const actions: Actions = {
const from_date = form.get('from_date') as string;
const to_date = form.get('to_date') as string;
const notes = (form.get('notes') as string) || undefined;
const include_minoxidil_beard = form.get('include_minoxidil_beard') === 'on';
if (!from_date || !to_date) {
return fail(400, { error: 'Daty początkowa i końcowa są wymagane.' });
@ -44,7 +51,7 @@ export const actions: Actions = {
}
try {
const batch = await suggestBatch({ from_date, to_date, notes });
const batch = await suggestBatch({ from_date, to_date, notes, include_minoxidil_beard });
return { batch, from_date, to_date };
} catch (e) {
return fail(502, { error: (e as Error).message });

View file

@ -1,5 +1,6 @@
<script lang="ts">
import { enhance } from '$app/forms';
import { resolve } from '$app/paths';
import { SvelteSet } from 'svelte/reactivity';
import type { ActionData, PageData } from './$types';
import type { BatchSuggestion, RoutineSuggestion, SuggestedStep } from '$lib/types';
@ -104,7 +105,7 @@
<div class="max-w-2xl space-y-6">
<div class="flex items-center gap-4">
<a href="/routines" class="text-sm text-muted-foreground hover:underline">{m["suggest_backToRoutines"]()}</a>
<a href={resolve('/routines')} class="text-sm text-muted-foreground hover:underline">{m["suggest_backToRoutines"]()}</a>
<h2 class="text-2xl font-bold tracking-tight">{m.suggest_title()}</h2>
</div>
@ -152,6 +153,18 @@
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
></textarea>
</div>
<div class="flex items-start gap-3 rounded-md border border-border px-3 py-2">
<input
id="single_include_minoxidil_beard"
name="include_minoxidil_beard"
type="checkbox"
class="mt-0.5 h-4 w-4 rounded border-input"
/>
<div class="space-y-0.5">
<Label for="single_include_minoxidil_beard" class="font-medium">{m["suggest_minoxidilToggleLabel"]()}</Label>
<p class="text-xs text-muted-foreground">{m["suggest_minoxidilToggleHint"]()}</p>
</div>
</div>
<Button type="submit" disabled={loadingSingle} class="w-full">
{#if loadingSingle}
@ -247,6 +260,18 @@
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring resize-none"
></textarea>
</div>
<div class="flex items-start gap-3 rounded-md border border-border px-3 py-2">
<input
id="batch_include_minoxidil_beard"
name="include_minoxidil_beard"
type="checkbox"
class="mt-0.5 h-4 w-4 rounded border-input"
/>
<div class="space-y-0.5">
<Label for="batch_include_minoxidil_beard" class="font-medium">{m["suggest_minoxidilToggleLabel"]()}</Label>
<p class="text-xs text-muted-foreground">{m["suggest_minoxidilToggleHint"]()}</p>
</div>
</div>
<Button type="submit" disabled={loadingBatch} class="w-full">
{#if loadingBatch}