Add phase 4: AI cleanup and photo/text import via OpenAI
- recipes/ai.py: Chat Completions with strict JSON schema (Structured Outputs), Pydantic re-validation, plausibility clamping, prompt treats input as data only - "Mit KI aufbereiten" on create/edit form returns an editable draft - Import page: photo/screenshot (vision, downscaled JPEG, not stored) and pasted text; drafts pre-fill the recipe form with a preview - Clear UI messages for timeout, rate limit, quota, auth, model and connection errors; without OPENAI_API_KEY AI features show a notice - Optional per-user and global daily AI limits (default unlimited) - Tests for response parsing, schema strictness, error mapping, views Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9256e94286
commit
1bf8ef8b65
@@ -0,0 +1 @@
|
||||
<p class="msg msg-warning">Die KI-Funktionen sind nicht eingerichtet (kein <code>OPENAI_API_KEY</code>). Alles andere funktioniert normal – du kannst Rezepte von Hand eintragen.</p>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="msg msg-warning draft-notice">
|
||||
<strong>{{ draft_notice|default:"KI-Entwurf" }} – bitte prüfen.</strong>
|
||||
Noch nichts ist gespeichert. Korrigiere die Felder unten bei Bedarf und tippe dann auf „Speichern“.
|
||||
</div>
|
||||
{% if ai_draft %}
|
||||
<details class="card preview" open>
|
||||
<summary><strong>Vorschau: {{ ai_draft.title|default:"Ohne Titel" }}</strong></summary>
|
||||
{% if ai_draft.ingredients %}
|
||||
<h3>Zutaten</h3>
|
||||
<ul class="ingredients">
|
||||
{% for ing in ai_draft.ingredients %}
|
||||
<li><span class="amount">{% if ing.amount is not None %}{{ ing.amount|floatformat:"-2" }}{% if ing.amount_max is not None %}–{{ ing.amount_max|floatformat:"-2" }}{% endif %}{% endif %} {{ ing.unit }}</span>
|
||||
<span>{% if ing.group %}<span class="muted">{{ ing.group }}: </span>{% endif %}{{ ing.name }}{% if ing.note %}<span class="muted">, {{ ing.note }}</span>{% endif %}</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if ai_draft.steps %}
|
||||
<h3>Zubereitung</h3>
|
||||
<ol class="steps">{% for step in ai_draft.steps %}<li>{{ step }}</li>{% endfor %}</ol>
|
||||
{% endif %}
|
||||
{% if ai_draft.tags %}<p class="chips">{% for tag in ai_draft.tags %}<span class="chip">#{{ tag }}</span>{% endfor %}</p>{% endif %}
|
||||
</details>
|
||||
{% endif %}
|
||||
@@ -3,17 +3,19 @@
|
||||
{% block content %}
|
||||
<h1>{% if is_new %}Neues Rezept{% else %}Rezept bearbeiten{% endif %}</h1>
|
||||
|
||||
{% block before_form %}{% endblock %}
|
||||
{% if draft_notice or ai_draft %}{% include "recipes/_draft_preview.html" %}{% endif %}
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="card recipe-form" novalidate>
|
||||
<form method="post"{% if form_action %} action="{{ form_action }}"{% endif %} enctype="multipart/form-data" class="card recipe-form" novalidate>
|
||||
{% csrf_token %}
|
||||
{% include "form_fields.html" %}
|
||||
{% block form_buttons %}
|
||||
<div class="form-buttons">
|
||||
<button type="submit" class="button">Speichern</button>
|
||||
{% if ai_enabled %}
|
||||
<button type="submit" name="ai" value="1" class="button secondary" data-busy-text="KI arbeitet …">✨ Mit KI aufbereiten</button>
|
||||
{% endif %}
|
||||
<a class="button secondary" href="{% if recipe %}{% url 'recipes:detail' recipe.pk %}{% else %}{% url 'recipes:list' %}{% endif %}">Abbrechen</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% if ai_enabled %}<p class="muted small">„Mit KI aufbereiten“ strukturiert deine Eingaben (Zutaten, Schritte, Tags) als Entwurf. Fotos danach auswählen – sie werden dabei nicht übernommen.</p>{% endif %}
|
||||
</form>
|
||||
|
||||
{% if recipe and images %}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Rezept importieren{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Rezept importieren</h1>
|
||||
<p class="muted">Das Ergebnis ist immer ein Entwurf, den du prüfst und erst dann speicherst.</p>
|
||||
|
||||
{% block link_import %}{% endblock %}
|
||||
|
||||
<section class="card">
|
||||
<h2>📷 Foto oder Screenshot</h2>
|
||||
{% if ai_enabled %}
|
||||
<form method="post" action="{% url 'recipes:import_photo' %}" enctype="multipart/form-data" novalidate>
|
||||
{% csrf_token %}
|
||||
{% with form=photo_form %}{% include "form_fields.html" %}{% endwith %}
|
||||
<p class="muted small">Die Bilder werden zur Auswertung an OpenAI geschickt und nicht gespeichert.</p>
|
||||
<button type="submit" class="button" data-busy-text="KI liest das Rezept …">Rezept erkennen</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% include "recipes/_ai_disabled.html" %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>📝 Text einfügen</h2>
|
||||
{% if ai_enabled %}
|
||||
<form method="post" action="{% url 'recipes:import_text' %}" novalidate>
|
||||
{% csrf_token %}
|
||||
{% with form=text_form %}{% include "form_fields.html" %}{% endwith %}
|
||||
<button type="submit" class="button" data-busy-text="KI bereitet auf …">Mit KI aufbereiten</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% include "recipes/_ai_disabled.html" %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<p><a href="{% url 'recipes:create' %}">Oder ganz von Hand eintragen →</a></p>
|
||||
{% endblock %}
|
||||
@@ -3,7 +3,10 @@
|
||||
{% block content %}
|
||||
<div class="page-head">
|
||||
<h1>Rezepte</h1>
|
||||
<a class="button" href="{% url 'recipes:create' %}">+ Neu</a>
|
||||
<div class="head-actions">
|
||||
<a class="button secondary" href="{% url 'recipes:import' %}">Import</a>
|
||||
<a class="button" href="{% url 'recipes:create' %}">+ Neu</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="search" role="search">
|
||||
|
||||
Reference in New Issue
Block a user