KI-Backend: Antwortretten - Funktion
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-09-04 12:10:42 +02:00
parent 7ef0c95a73
commit fedfceb81d
10 changed files with 435 additions and 14 deletions
@@ -1491,10 +1491,16 @@ public partial class AiAssistDialogViewModel : ObservableObject
[ObservableProperty] private string _errorMessage = "";
[ObservableProperty] private bool _hasResults;
[ObservableProperty] private string? _summary;
[ObservableProperty] private string? _rawResponse;
public string UnitSummary { get; }
public ObservableCollection<AiLessonReviewItem> ReviewItems { get; } = [];
public bool Result { get; private set; }
public Unit Unit => _unit;
public Guid? FocusLessonId => _focusLesson?.Id;
public bool CanRescueResponse => !string.IsNullOrWhiteSpace(RawResponse);
partial void OnRawResponseChanged(string? value) => OnPropertyChanged(nameof(CanRescueResponse));
/// Aus dem Editor einer einzelnen Stunde heraus gestartet (statt aus der Einheiten-Übersicht,
/// Nutzer-Feedback nach den ersten Live-Tests) — die KI darf dann ausschließlich diese eine
@@ -1533,7 +1539,7 @@ public partial class AiAssistDialogViewModel : ObservableObject
? ReviewItems.Where(i => i.Accepted).Select(i => i.Source).ToList()
: null;
ErrorMessage = ""; IsBusy = true;
ErrorMessage = ""; RawResponse = null; IsBusy = true;
try
{
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token, AllowModifyingExisting,
@@ -1565,7 +1571,11 @@ public partial class AiAssistDialogViewModel : ObservableObject
Summary = response.Summary;
HasResults = true;
}
catch (AiBackendException ex) { ErrorMessage = ex.Message; }
catch (AiBackendException ex)
{
ErrorMessage = ex.Message;
RawResponse = ex.RawResponse;
}
finally { IsBusy = false; }
}
@@ -1579,6 +1589,132 @@ public partial class AiAssistDialogViewModel : ObservableObject
}
[RelayCommand] private void Cancel() => Result = false;
public void MarkRescueImported() => Result = true;
}
/// <summary>Aus einer manuell geprüften KI-Antwort auswählbare Stunde.</summary>
public record AiRescueLessonOption(AiLesson Lesson, string Label);
/// <summary>Ziel für den manuellen Import in eine bereits vorhandene Stunde.</summary>
public record AiRescueTargetOption(Lesson Lesson, string Label);
/// <summary>
/// Rettungsdialog für syntaktisch fehlerhafte oder mit Freitext vermischte Modellantworten. Der
/// Nutzer entscheidet selbst, welcher Textabschnitt geparst und wohin die Stunde importiert wird.
/// </summary>
public partial class AiResponseRescueDialogViewModel : ObservableObject
{
private readonly AiPlanningService _aiPlanning;
private readonly ILessonRepository _lessons;
private readonly Unit _unit;
[ObservableProperty] private string _responseText;
[ObservableProperty] private string _parseMessage =
"Markiere den gültigen JSON-Abschnitt oder bearbeite den Text und klicke auf „Markierung prüfen“.";
[ObservableProperty] private AiRescueLessonOption? _selectedParsedLesson;
[ObservableProperty] private AiRescueTargetOption? _selectedTarget;
public ObservableCollection<AiRescueLessonOption> ParsedLessons { get; } = [];
public ObservableCollection<AiRescueTargetOption> ExistingLessons { get; } = [];
public bool HasParsedLesson => SelectedParsedLesson is not null;
public bool CanImportIntoExisting => SelectedParsedLesson is not null && SelectedTarget is not null;
public bool Result { get; private set; }
partial void OnSelectedParsedLessonChanged(AiRescueLessonOption? value)
{
OnPropertyChanged(nameof(HasParsedLesson));
OnPropertyChanged(nameof(CanImportIntoExisting));
}
partial void OnSelectedTargetChanged(AiRescueTargetOption? value) =>
OnPropertyChanged(nameof(CanImportIntoExisting));
public AiResponseRescueDialogViewModel(AiPlanningService aiPlanning, ILessonRepository lessons,
Unit unit, string rawResponse, Guid? preferredTargetId = null)
{
_aiPlanning = aiPlanning;
_lessons = lessons;
_unit = unit;
_responseText = rawResponse;
foreach (var lesson in lessons.GetByUnit(unit.Id).OrderBy(l => l.Date).ThenBy(l => l.LessonNumber))
{
var date = lesson.Date.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture);
var number = lesson.LessonNumber is { } n ? $", Stunde {n}" : "";
ExistingLessons.Add(new AiRescueTargetOption(lesson, $"{date}{number}: {lesson.Topic}"));
}
SelectedTarget = ExistingLessons.FirstOrDefault(x => x.Lesson.Id == preferredTargetId)
?? ExistingLessons.FirstOrDefault();
}
public void ParseSelection(string selectedText)
{
ParsedLessons.Clear();
SelectedParsedLesson = null;
try
{
var parsed = AiPlanningService.ParsePlanningLessons(selectedText);
foreach (var lesson in parsed)
{
var date = lesson.Date?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "ohne Datum";
ParsedLessons.Add(new AiRescueLessonOption(lesson, $"{date}: {lesson.Topic}"));
}
SelectedParsedLesson = ParsedLessons[0];
ParseMessage = parsed.Count == 1
? "Eine gültige Stunde erkannt."
: $"{parsed.Count} gültige Stunden erkannt. Bitte die gewünschte Stunde auswählen.";
}
catch (AiBackendException ex)
{
ParseMessage = ex.Message;
}
}
[RelayCommand]
private void ImportIntoExisting()
{
if (SelectedParsedLesson is null || SelectedTarget is null) return;
var lesson = CloneForImport(SelectedParsedLesson.Lesson, SelectedTarget.Lesson.Id);
Save([lesson], focusLessonId: SelectedTarget.Lesson.Id);
}
[RelayCommand]
private void ImportAsNew()
{
if (SelectedParsedLesson is null) return;
Save([CloneForImport(SelectedParsedLesson.Lesson, null)]);
}
private void Save(List<AiLesson> source, Guid? focusLessonId = null)
{
foreach (var lesson in _aiPlanning.ApplyResponse(_unit, source,
allowModifyingExistingLessons: true, focusLessonId))
_lessons.Save(lesson);
Result = true;
}
private static AiLesson CloneForImport(AiLesson source, Guid? id) => new()
{
Id = id,
Date = source.Date,
LessonNumber = source.LessonNumber,
Topic = source.Topic,
StartTime = source.StartTime,
Homework = source.Homework,
Reflection = source.Reflection,
Phases = source.Phases.Select(p => new AiPhaseStep
{
Name = p.Name,
DurationMinutes = p.DurationMinutes,
Activity = p.Activity,
Material = p.Material,
Shorthand = p.Shorthand,
AlternativePathName = p.AlternativePathName,
MaterialSuggestion = p.MaterialSuggestion,
}).ToList(),
};
}
// ── Dialog: Einheit als Vorlage in andere Gruppe kopieren (4.1.4) ────────────