Files
Kochbuch/templates/recipes/list.html
T
Kyle DammannandClaude Opus 5.5 1bf8ef8b65 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]>
2026-10-30 21:43:30 +01:00

77 lines
2.8 KiB
HTML

{% extends "base.html" %}
{% block title %}Rezepte{% endblock %}
{% block content %}
<div class="page-head">
<h1>Rezepte</h1>
<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">
<div class="search-row">
{{ form.q }}
<button type="submit" class="button">Suchen</button>
</div>
<details class="filters"{% if form.zutat.value or form.ansicht.value %} open{% endif %}>
<summary>Filter</summary>
<div class="field"><label for="{{ form.zutat.id_for_label }}">{{ form.zutat.label }}</label>{{ form.zutat }}</div>
<div class="field"><label for="{{ form.ansicht.id_for_label }}">{{ form.ansicht.label }}</label>{{ form.ansicht }}</div>
</details>
{% if active_tag %}<input type="hidden" name="tag" value="{{ active_tag }}">{% endif %}
</form>
{% if tags %}
<nav class="chips" aria-label="Tags">
{% for tag in tags %}
{% if tag.name == active_tag %}
<a class="chip active" href="{% querystring tag=None seite=None %}">#{{ tag.name }} ✕</a>
{% else %}
<a class="chip" href="{% querystring tag=tag.name seite=None %}">#{{ tag.name }}</a>
{% endif %}
{% endfor %}
</nav>
{% endif %}
{% if page.object_list %}
<ul class="recipe-grid">
{% for recipe in page.object_list %}
<li>
<a class="recipe-card" href="{% url 'recipes:detail' recipe.pk %}">
{% with cover=recipe.cover %}
{% if cover %}
<img src="{% url 'recipes:photo' cover.uuid 'klein' %}" alt="" loading="lazy" width="480" height="360">
{% else %}
<div class="placeholder" aria-hidden="true">🍲</div>
{% endif %}
{% endwith %}
<div class="recipe-card-body">
<strong>{{ recipe.title }}</strong>
<span class="muted">
{% if recipe.total_minutes %}{{ recipe.total_minutes }} Min. · {% endif %}{{ recipe.owner.display_name }}
{% if not recipe.is_public %} · 🔒{% endif %}{% if recipe.is_favorite %} · ★{% endif %}
</span>
</div>
</a>
</li>
{% endfor %}
</ul>
{% if page.has_other_pages %}
<nav class="pager">
{% if page.has_previous %}<a class="button secondary" href="{% querystring seite=page.previous_page_number %}">← Zurück</a>{% endif %}
<span class="muted">Seite {{ page.number }} von {{ page.paginator.num_pages }}</span>
{% if page.has_next %}<a class="button secondary" href="{% querystring seite=page.next_page_number %}">Weiter →</a>{% endif %}
</nav>
{% endif %}
{% elif filtered %}
<p class="muted">Keine Rezepte gefunden. <a href="{% url 'recipes:list' %}">Filter zurücksetzen</a></p>
{% else %}
<div class="card empty">
<p>Noch keine Rezepte. Leg das erste an!</p>
<a class="button" href="{% url 'recipes:create' %}">Rezept anlegen</a>
</div>
{% endif %}
{% endblock %}