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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,62 @@
|
||||
using LehrerApp.Core.AiPlanning;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace LehrerApp.Desktop.Services;
|
||||
|
||||
/// <summary>Fehler beim Aufruf des KI-Backends, Message ist bereits deutsch und nutzergerichtet.</summary>
|
||||
public class AiBackendException(string userMessage) : Exception(userMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Der KI-Wire-Vertrag verwendet deutsches Datumsformat (siehe ai-backend/plan.php Systemprompt,
|
||||
/// Abschnitt "Eingabeschema"/"Antwortformat"), nicht .NETs Standardformat für DateOnly (ISO
|
||||
/// "yyyy-MM-dd") — ohne diesen Converter würde jede im dokumentierten Format zurückgegebene
|
||||
/// KI-Antwort beim Deserialisieren mit einer JsonException scheitern. Fällt defensiv auf
|
||||
/// allgemeines Parsen zurück, falls die KI sich nicht exakt ans Format hält, statt hart zu
|
||||
/// scheitern — LLMs weichen erfahrungsgemäß gelegentlich vom dokumentierten Format ab.
|
||||
/// </summary>
|
||||
public class GermanDateOnlyJsonConverter : JsonConverter<DateOnly>
|
||||
{
|
||||
private const string Format = "dd.MM.yyyy";
|
||||
|
||||
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var value = reader.GetString() ?? "";
|
||||
if (DateOnly.TryParseExact(value, Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact))
|
||||
return exact;
|
||||
if (DateOnly.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var fallback))
|
||||
return fallback;
|
||||
throw new JsonException($"Datum \"{value}\" konnte nicht gelesen werden (erwartet: {Format}).");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) =>
|
||||
writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>Analog zu <see cref="GermanDateOnlyJsonConverter"/>, für "HH:mm" statt ISO-Zeiten.</summary>
|
||||
public class GermanTimeOnlyJsonConverter : JsonConverter<TimeOnly>
|
||||
{
|
||||
private const string Format = "HH:mm";
|
||||
|
||||
public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var value = reader.GetString() ?? "";
|
||||
if (TimeOnly.TryParseExact(value, Format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var exact))
|
||||
return exact;
|
||||
if (TimeOnly.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.None, out var fallback))
|
||||
return fallback;
|
||||
throw new JsonException($"Uhrzeit \"{value}\" konnte nicht gelesen werden (erwartet: {Format}).");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) =>
|
||||
writer.WriteStringValue(value.ToString(Format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Orchestriert die KI-gestützte Planungsunterstützung (TODO 4.5.9): baut aus einer <see cref="Unit"/>
|
||||
/// den Export-Kontext, ruft das externe PHP-Backend (ai-backend/) auf und wendet dessen Antwort auf
|
||||
@@ -26,6 +73,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
PropertyNameCaseInsensitive = true,
|
||||
Converters = { new GermanDateOnlyJsonConverter(), new GermanTimeOnlyJsonConverter() },
|
||||
};
|
||||
|
||||
public async Task<string> LoginAsync(string username, string password)
|
||||
@@ -71,7 +119,12 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
}
|
||||
|
||||
/// <summary>Rein (nur Repository-Lesezugriffe, kein Netzwerk) — testbar mit Fakes.</summary>
|
||||
public AiUnitContext BuildContext(Unit unit, string instruction)
|
||||
/// <param name="draftOverrides">
|
||||
/// Noch nicht gespeicherter Entwurf aus einer vorherigen Antwortrunde (Nachfassen mit
|
||||
/// geänderter Anweisung, siehe <see cref="MergeDraft"/>) — überschreibt einzelne Lessons im
|
||||
/// gesendeten Kontext, ohne dass dafür etwas in der Datenbank gespeichert werden muss.
|
||||
/// </param>
|
||||
public AiUnitContext BuildContext(Unit unit, string instruction, List<AiLesson>? draftOverrides = null)
|
||||
{
|
||||
var group = groups.GetById(unit.GroupId);
|
||||
var subject = group?.SubjectId is { } subjectId ? subjects.GetById(subjectId) : null;
|
||||
@@ -114,6 +167,9 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (draftOverrides is not null)
|
||||
unitLessons = MergeDraft(unitLessons, draftOverrides);
|
||||
|
||||
return new AiUnitContext
|
||||
{
|
||||
Id = unit.Id,
|
||||
@@ -131,13 +187,29 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ersetzt in <paramref name="saved"/> (dem tatsächlichen Datenbankstand) jede Lesson, deren Id
|
||||
/// auch in <paramref name="draft"/> vorkommt, durch die Entwurfsversion, und hängt Entwürfe ohne
|
||||
/// Id (neu vorgeschlagene Lessons) an. So sieht die KI beim Nachfassen den Stand inklusive der
|
||||
/// zuvor vorgeschlagenen, aber noch nicht gespeicherten Änderungen — ohne dass dafür etwas in
|
||||
/// der Datenbank landen muss, bevor der Nutzer "Übernehmen" klickt.
|
||||
/// </summary>
|
||||
private static List<AiLesson> MergeDraft(List<AiLesson> saved, List<AiLesson> draft)
|
||||
{
|
||||
var draftById = draft.Where(d => d.Id.HasValue).ToDictionary(d => d.Id!.Value);
|
||||
var merged = saved.Select(l => l.Id.HasValue && draftById.TryGetValue(l.Id.Value, out var replacement)
|
||||
? replacement : l).ToList();
|
||||
merged.AddRange(draft.Where(d => !d.Id.HasValue));
|
||||
return merged;
|
||||
}
|
||||
|
||||
public async Task<AiPlanningResponse> RequestPlanAsync(Unit unit, string instruction, string token,
|
||||
bool allowModifyingExistingLessons = true)
|
||||
bool allowModifyingExistingLessons = true, List<AiLesson>? draftOverrides = null)
|
||||
{
|
||||
var request = new AiPlanningRequest
|
||||
{
|
||||
Instruction = instruction,
|
||||
Unit = BuildContext(unit, instruction),
|
||||
Unit = BuildContext(unit, instruction, draftOverrides),
|
||||
AllowModifyingExistingLessons = allowModifyingExistingLessons,
|
||||
};
|
||||
|
||||
|
||||
@@ -1141,10 +1141,17 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
||||
return;
|
||||
}
|
||||
|
||||
// Nachfassen (HasResults bereits true): die aktuell angehakten Vorschläge der letzten Runde
|
||||
// als Entwurf mitschicken, damit die KI auf dem bereits gezeigten, noch nicht gespeicherten
|
||||
// Stand aufbaut statt nur auf dem tatsächlichen Datenbankstand der Einheit.
|
||||
var draftOverrides = HasResults
|
||||
? ReviewItems.Where(i => i.Accepted).Select(i => i.Source).ToList()
|
||||
: null;
|
||||
|
||||
ErrorMessage = ""; IsBusy = true;
|
||||
try
|
||||
{
|
||||
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token, AllowModifyingExisting);
|
||||
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token, AllowModifyingExisting, draftOverrides);
|
||||
var existingIds = _lessons.GetByUnit(_unit.Id).Select(l => l.Id).ToHashSet();
|
||||
|
||||
ReviewItems.Clear();
|
||||
|
||||
@@ -13,20 +13,6 @@
|
||||
<TextBlock Text="KI-Unterstützung" Classes="dialogtitle"/>
|
||||
<TextBlock Text="{Binding UnitSummary}" FontSize="12" Opacity="0.6"/>
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding !HasResults}">
|
||||
<TextBlock Text="Anweisung" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding Instruction}" AcceptsReturn="True" TextWrapping="Wrap" Height="120"
|
||||
PlaceholderText="z.B. Ergänze zwei weitere Stunden zum Thema Redoxreaktionen mit steigendem Anspruch."
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
<CheckBox Content="Auch bestehende Stundeninhalte anpassen" IsChecked="{Binding AllowModifyingExisting}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Deaktivieren, um die Einheit nur um neue Stunden zu erweitern, ohne den Inhalt bereits vorhandener Stunden zu verändern."/>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Anfrage läuft…" FontSize="12" Opacity="0.6" IsVisible="{Binding IsBusy}"/>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding ErrorMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
|
||||
<StackPanel Spacing="10" IsVisible="{Binding HasResults}">
|
||||
<TextBlock Text="{Binding Summary}" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Summary, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
@@ -41,14 +27,35 @@
|
||||
<TextBlock Text="Die KI hat keine Stunden vorgeschlagen." Opacity="0.6" FontSize="12"
|
||||
IsVisible="{Binding !ReviewItems.Count}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Weitere Anweisung (nachfassen, baut auf den angehakten Vorschlägen oben auf)"
|
||||
FontSize="12" Opacity="0.7" IsVisible="{Binding HasResults}"/>
|
||||
<TextBlock Text="Anweisung" FontSize="12" Opacity="0.7" IsVisible="{Binding !HasResults}"/>
|
||||
<TextBox Text="{Binding Instruction}" AcceptsReturn="True" TextWrapping="Wrap" Height="120"
|
||||
PlaceholderText="z.B. Ergänze zwei weitere Stunden zum Thema Redoxreaktionen mit steigendem Anspruch."
|
||||
IsEnabled="{Binding !IsBusy}"/>
|
||||
<CheckBox Content="Auch bestehende Stundeninhalte anpassen" IsChecked="{Binding AllowModifyingExisting}"
|
||||
IsEnabled="{Binding !IsBusy}"
|
||||
ToolTip.Tip="Deaktivieren, um die Einheit nur um neue Stunden zu erweitern, ohne den Inhalt bereits vorhandener Stunden zu verändern."/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4" IsVisible="{Binding IsBusy}">
|
||||
<TextBlock Text="Anfrage läuft…" FontSize="12" Opacity="0.6"/>
|
||||
<ProgressBar IsIndeterminate="True" Height="4"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" FontSize="12" TextWrapping="Wrap"
|
||||
IsVisible="{Binding ErrorMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
|
||||
<Grid Grid.Row="1" ColumnDefinitions="*,8,*,8,*" Margin="0,20,0,0">
|
||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="Anfrage senden" HorizontalAlignment="Stretch" Click="OnSend"
|
||||
IsVisible="{Binding !HasResults}" IsEnabled="{Binding !IsBusy}"/>
|
||||
<Button Grid.Column="2" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnApply"
|
||||
<Button Grid.Column="2" Content="Erneut anfragen" HorizontalAlignment="Stretch" Click="OnSend"
|
||||
IsVisible="{Binding HasResults}" IsEnabled="{Binding !IsBusy}"/>
|
||||
<Button Grid.Column="4" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnApply"
|
||||
IsVisible="{Binding HasResults}"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -690,6 +690,20 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
||||
ob Anthropic zuverlässig valides JSON im erwarteten Schema liefert, ob die berechneten
|
||||
Kosten exakt mit der echten Abrechnung übereinstimmen, sowie die komplette Kette
|
||||
Desktop → Backend → Anthropic unter echten Netzwerkbedingungen.
|
||||
|
||||
**Nachtrag (Bugfix, aus echtem Live-Test):** Erste echte Planungsanfrage lieferte beim
|
||||
Verarbeiten der Antwort eine `JsonException`. Ursache: `AiPlanningService.JsonOptions` hatte
|
||||
keinen Converter für `DateOnly`/`TimeOnly` registriert, sodass .NETs Standardformat (ISO 8601,
|
||||
z.B. "2026-12-24") verwendet wurde — der Systemprompt in `plan.php` dokumentiert aber
|
||||
durchgängig deutsches Format ("TT.MM.JJJJ"/"HH:mm"). Das betraf beide Richtungen: die
|
||||
ausgehende Anfrage wurde entgegen der Prompt-Dokumentation im ISO-Format gesendet, und eine
|
||||
dem Prompt folgende KI-Antwort im deutschen Format scheiterte beim Deserialisieren. Behoben
|
||||
durch `GermanDateOnlyJsonConverter`/`GermanTimeOnlyJsonConverter` (in `AiPlanningService.cs`),
|
||||
die deutsches Format schreiben und beim Lesen zuerst deutsches Format, dann defensiv
|
||||
allgemeines Parsen (u.a. ISO) versuchen — LLMs weichen erfahrungsgemäß gelegentlich vom
|
||||
dokumentierten Format ab, ein harter Fehlschlag bei kleinen Abweichungen wäre unnötig
|
||||
fragil. Abgesichert durch `GermanDateTimeJsonConverterTests.cs`, u.a. mit dem konkreten
|
||||
nullable-Feld-Fall (`AiLesson.Date`/`StartTime`), der den ursprünglichen Bug ausgelöst hat.
|
||||
- [ ] **4.5.10** Falls doch ein schlanker Companion-/WebApp-Client entstehen soll: bewusst
|
||||
**minimaler** Funktionsumfang — nur Wochenraster ansehen, eine Stunde verschieben, oder eine
|
||||
Stunde als "Umplanung nötig" flaggen. Kein Editor für Einheiten/Kompetenzen/KI-Planung dort.
|
||||
@@ -762,6 +776,24 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
||||
**Nicht ohne echten API-Key verifizierbar**, ob Caching tatsächlich greift — nur an
|
||||
`cache_read_input_tokens > 0` in `transactions` bei einer zweiten Anfrage innerhalb der
|
||||
TTL beobachtbar (siehe `ai-backend/README.md`).
|
||||
- [x] **4.5.17** Fortschrittsanzeige + einfaches Nachfassen im `AiAssistDialog` (Nutzer-Feedback
|
||||
nach erstem echten Live-Test: Dialog wirkte während der Anfrage "aufgehängt", und eine
|
||||
geänderte Anweisung erforderte bisher komplettes Abbrechen/Neustarten). **Umsetzung:**
|
||||
indeterminierter `ProgressBar` unter "Anfrage läuft…". Das Anweisungsfeld bleibt nach der
|
||||
ersten Antwort sichtbar (statt zu verschwinden); ein zweiter Button "Erneut anfragen" sendet
|
||||
eine geänderte/ergänzte Anweisung erneut, wobei die aktuell angehakten Vorschläge der letzten
|
||||
Runde als Entwurfskontext mitgeschickt werden (`AiPlanningService.MergeDraft`: ersetzt
|
||||
Lessons mit passender Id im tatsächlichen Datenbankstand durch die Entwurfsversion, hängt
|
||||
neue Vorschläge ohne Id an) — die KI baut so auf dem bereits gezeigten, noch nicht
|
||||
gespeicherten Stand auf, ohne dass dafür etwas in der Datenbank landen muss. Ergebnisliste
|
||||
wird bei jeder Runde einfach ersetzt, **kein** Seite-an-Seite-Vergleich zwischen den Runden
|
||||
(das wäre 4.5.18). Bewusst einfacherer erster Schritt statt des größeren Umbaus.
|
||||
- [ ] **4.5.18** (Zurückgestellt, Nutzer-Idee zu 4.5.17) Statt die Ergebnisliste beim Nachfassen
|
||||
einfach zu ersetzen: alten und neuen KI-Entwurf gegenüberstellen und den Nutzer pro Stunde
|
||||
(oder insgesamt) wählen lassen, mit welchem Stand weitergemacht wird. Größerer Umbau als
|
||||
4.5.17 (zwei Entwürfe parallel im Speicher halten, Seite-an-Seite-UI, Entscheidung je Stunde
|
||||
statt nur global) — deshalb erst mal zurückgestellt, bis sich zeigt, ob das einfache
|
||||
Nachfassen aus 4.5.17 in der Praxis schon ausreicht.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user