KI-Feature 4.5.21: didaktischer Hintergrund je Stunde, nur auf Nachfrage

Neuer Endpunkt ai-backend/explain.php mit eigenem statischen Systemprompt
(Begründung des Phasenaufbaus, mögliche Stolpersteine, Differenzierungsideen).
Bewusst als separater Endpunkt statt Zusatzfeld in jeder plan.php-Antwort,
damit die Erklärung nur bei tatsächlicher Nutzung abgerechnet wird statt bei
jeder Planungsanfrage mitgeneriert zu werden.

Die Guthaben-Abrechnung (SELECT-FOR-UPDATE, Transaktions-Insert) wurde aus
plan.php nach ai_backend_call_and_charge in db.php ausgelagert, damit sie
nicht an zwei Stellen gepflegt werden muss. Kein neues DB-Schema nötig.

Im AiAssistDialog erscheint je Stunde ein Button "Didaktischen Hintergrund
erklären", der nach dem Laden durch den Text ersetzt wird.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 22:32:36 +02:00
co-authored by Claude Sonnet 5
parent f703fe7b66
commit 364df378f0
11 changed files with 333 additions and 79 deletions
@@ -339,6 +339,47 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
}
}
/// <summary>
/// Fragt einen didaktischen Hintergrund zu einer bereits vorgeschlagenen/geplanten Lesson ab
/// (4.5.21 "Schattenfeld") — eigener Endpunkt (ai-backend/explain.php), damit das nur bei
/// tatsächlicher Nutzung abgerechnet wird statt bei jeder plan.php-Antwort mitgeneriert zu
/// werden. Ändert nichts an der Lesson, liefert nur erklärenden Text.
/// </summary>
public async Task<string> RequestExplanationAsync(Unit unit, AiLesson lesson, string token)
{
var request = new AiExplainRequest { Unit = BuildContext(unit, ""), Lesson = lesson };
using var req = new HttpRequestMessage(HttpMethod.Post, "explain.php")
{
Content = JsonContent.Create(request, options: JsonOptions),
};
req.Headers.Authorization = new("Bearer", token);
HttpResponseMessage resp;
try { resp = await http.SendAsync(req); }
catch (HttpRequestException)
{
throw new AiBackendException("Der KI-Dienst ist nicht erreichbar. Bitte Internetverbindung prüfen.");
}
if (resp.StatusCode == HttpStatusCode.Unauthorized)
throw new AiBackendException("Anmeldung abgelaufen. Bitte in den Einstellungen erneut anmelden.");
if (resp.StatusCode == (HttpStatusCode)402)
throw new AiBackendException("Nicht genügend KI-Guthaben. Bitte Guthaben aufladen.");
if (!resp.IsSuccessStatusCode)
throw new AiBackendException("Die Anfrage an den KI-Dienst ist fehlgeschlagen.");
try
{
var result = await resp.Content.ReadFromJsonAsync<AiExplainResponse>(JsonOptions);
return result?.Explanation ?? throw new AiBackendException("Die Antwort der KI konnte nicht verarbeitet werden.");
}
catch (Exception ex) when (ex is not AiBackendException)
{
throw new AiBackendException("Die Antwort der KI konnte nicht verarbeitet werden. Bitte erneut versuchen.");
}
}
/// <summary>
/// Rein (nur Repository-Lesezugriff für den Alternativpfad-Katalog, kein Schreiben) — testbar
/// mit Fakes. Gibt die zu speichernden Lesson-Objekte zurück; der Aufrufer ruft