KI-Backend: deutsches Datumsformat fixen, Fortschrittsanzeige + Nachfassen im Dialog

Live-Test lieferte JsonException: DateOnly/TimeOnly hatten keinen Converter
fürs im Systemprompt dokumentierte deutsche Format (TT.MM.JJJJ/HH:mm), .NET
nutzte stattdessen ISO 8601 in beide Richtungen. Neue Converter mit Fallback
aufs allgemeine Parsen.

AiAssistDialog: indeterminierter ProgressBar statt nur Text während der
Anfrage. Neuer Button "Erneut anfragen" erlaubt Nachfassen mit geänderter
Anweisung, ohne den Dialog neu zu starten — schickt die aktuell angehakten
Vorschläge als Entwurfskontext mit (AiPlanningService.MergeDraft), damit die
KI auf dem noch ungespeicherten Stand aufbaut. Side-by-side-Vergleich beider
Entwürfe als 4.5.18 zurückgestellt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 16:39:46 +02:00
co-authored by Claude Sonnet 5
parent 1928916eac
commit 2005b73a16
6 changed files with 258 additions and 20 deletions
@@ -103,6 +103,46 @@ public sealed class AiPlanningServiceTests
Assert.Contains(context.AlternativePathCatalog, p => p.Name == "Vertiefung" && p.Id == path.Id);
}
[Fact]
public void BuildContext_DraftOverride_ErsetztBestehendeLessonMitGleicherId()
{
var group = new LearningGroup();
var unit = new Unit { GroupId = group.Id, Title = "T" };
var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Alter Titel" };
var lessons = new FakeLessons();
lessons.Add(existing);
var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]),
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
var draft = new List<AiLesson> { new() { Id = existing.Id, Topic = "Neuer Entwurfstitel", Phases = [] } };
var context = service.BuildContext(unit, "", draft);
var aiLesson = Assert.Single(context.Lessons);
Assert.Equal(existing.Id, aiLesson.Id);
Assert.Equal("Neuer Entwurfstitel", aiLesson.Topic);
}
[Fact]
public void BuildContext_DraftOverride_HaengtNeueEntwurfLessonsOhneIdAn()
{
var group = new LearningGroup();
var unit = new Unit { GroupId = group.Id, Title = "T" };
var existing = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Bestehend" };
var lessons = new FakeLessons();
lessons.Add(existing);
var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]),
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
var draft = new List<AiLesson> { new() { Id = null, Topic = "Neu aus Runde 1", Phases = [] } };
var context = service.BuildContext(unit, "", draft);
Assert.Equal(2, context.Lessons.Count);
Assert.Contains(context.Lessons, l => l.Topic == "Bestehend");
Assert.Contains(context.Lessons, l => l.Topic == "Neu aus Runde 1" && l.Id == null);
}
[Fact]
public void ApplyResponse_BekannteId_WirdAlsUpdateBehandelt()
{
@@ -0,0 +1,80 @@
using LehrerApp.Core.AiPlanning;
using LehrerApp.Desktop.Services;
using System.Text.Json;
using Xunit;
namespace LehrerApp.Desktop.Tests;
/// <summary>
/// Deckt den Bug ab, der beim ersten echten Test der KI-Unterstützung auftrat: .NETs
/// DateOnly/TimeOnly haben standardmäßig ein ISO-JSON-Format ("yyyy-MM-dd"/"HH:mm:ss"), der
/// KI-Wire-Vertrag dokumentiert aber deutsches Format ("TT.MM.JJJJ"/"HH:mm") — jede KI-Antwort im
/// dokumentierten Format scheiterte deshalb mit einer JsonException beim Deserialisieren.
/// </summary>
public sealed class GermanDateTimeJsonConverterTests
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
Converters = { new GermanDateOnlyJsonConverter(), new GermanTimeOnlyJsonConverter() },
};
[Fact]
public void DateOnly_SerialisiertAlsDeutschesFormat()
{
Assert.Equal("\"24.12.2026\"", JsonSerializer.Serialize(new DateOnly(2026, 12, 24), Options));
}
[Fact]
public void DateOnly_ParstDeutschesFormat()
{
Assert.Equal(new DateOnly(2026, 12, 24), JsonSerializer.Deserialize<DateOnly>("\"24.12.2026\"", Options));
}
/// LLMs weichen erfahrungsgemäß gelegentlich vom im Prompt dokumentierten Format ab —
/// ISO 8601 wird deshalb defensiv trotzdem akzeptiert statt hart zu scheitern.
[Fact]
public void DateOnly_FaelltBeiAbweichendemFormatAufAllgemeinesParsenZurueck()
{
Assert.Equal(new DateOnly(2026, 12, 24), JsonSerializer.Deserialize<DateOnly>("\"2026-12-24\"", Options));
}
[Fact]
public void TimeOnly_SerialisiertAlsHHmm()
{
Assert.Equal("\"14:30\"", JsonSerializer.Serialize(new TimeOnly(14, 30), Options));
}
[Fact]
public void TimeOnly_ParstHHmm()
{
Assert.Equal(new TimeOnly(14, 30), JsonSerializer.Deserialize<TimeOnly>("\"14:30\"", Options));
}
/// Der eigentliche Bug betraf DateOnly?/TimeOnly? (nullable) in AiLesson, nicht die
/// nicht-nullable Basistypen — dieser Test deckt genau das ab, nicht nur die Converter isoliert.
[Fact]
public void AiLesson_MitDeutschemDatumUndUhrzeit_DeserialisiertKorrekt()
{
const string json = """
{"id":null,"date":"24.12.2026","lessonNumber":3,"topic":"Test","startTime":"14:30",
"phases":[],"homework":null,"reflection":null}
""";
var lesson = JsonSerializer.Deserialize<AiLesson>(json, Options);
Assert.Equal(new DateOnly(2026, 12, 24), lesson!.Date);
Assert.Equal(new TimeOnly(14, 30), lesson.StartTime);
}
[Fact]
public void AiLesson_OhneDatumUndUhrzeit_BleibtNull()
{
const string json = """{"id":null,"topic":"Test","phases":[]}""";
var lesson = JsonSerializer.Deserialize<AiLesson>(json, Options);
Assert.Null(lesson!.Date);
Assert.Null(lesson.StartTime);
}
}