This commit is contained in:
@@ -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) ────────────
|
||||
|
||||
Reference in New Issue
Block a user