KI-gestützte Planungsunterstützung (4.5.9) + Kompetenzkatalog-Import (8.1.2)
KI-Unterstützung: neuer Einstellungen-Tab (Anmeldung, Guthaben) und Button im Planungs-Tab, der Einheiten+Stunden als JSON an ein neues PHP-Backend (ai-backend/) sendet und die Antwort als prüfbare Vorschlagsliste zurückbringt. Provider-Aufruf, Guthabenverwaltung und Abrechnung nach echten Token-Kosten laufen serverseitig, der Desktop-Client sieht nie einen LLM-API-Key. Zentral abgesichert: eine von der KI zurückgegebene Stunden-Id, die zu keiner echten Lesson der Einheit passt, wird nie als Update übernommen, sondern immer als neue Stunde behandelt. Kompetenzkatalog-Import (8.1.2): JSON-Export/Import für Kompetenzkataloge.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LehrerApp.Core.AiPlanning;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
using LehrerApp.Desktop.Services;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
|
||||
@@ -29,6 +31,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
private readonly IGroupRepository _groups;
|
||||
private readonly ISubjectRepository _subjects;
|
||||
private readonly ICompetencyDomainRepository _competencyDomains;
|
||||
private readonly AiSettingsService _aiSettings;
|
||||
|
||||
private Guid _groupId;
|
||||
|
||||
@@ -66,13 +69,15 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
public Func<Lesson, Task<MoveLessonTarget?>>? OnPickMoveTarget { get; set; }
|
||||
public Func<Lesson, Task>? OnShowLesson { get; set; }
|
||||
public Func<Unit, Task<LessonSeriesResult?>>? OnGenerateLessonSeries { get; set; }
|
||||
public Func<Unit, Task<bool>>? OnAiAssist { get; set; }
|
||||
|
||||
public PlanningTabViewModel(IUnitRepository units, ILessonRepository lessons,
|
||||
IGroupRepository groups, ISubjectRepository subjects,
|
||||
ICompetencyDomainRepository competencyDomains)
|
||||
ICompetencyDomainRepository competencyDomains, AiSettingsService aiSettings)
|
||||
{
|
||||
_units = units; _lessons = lessons; _groups = groups;
|
||||
_subjects = subjects; _competencyDomains = competencyDomains;
|
||||
_aiSettings = aiSettings;
|
||||
}
|
||||
|
||||
public void Initialize(Guid groupId, bool isReadOnly = false)
|
||||
@@ -121,6 +126,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
CopyUnitCommand.NotifyCanExecuteChanged();
|
||||
AddLessonCommand.NotifyCanExecuteChanged();
|
||||
GenerateLessonSeriesCommand.NotifyCanExecuteChanged();
|
||||
AiAssistCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void LoadLessons()
|
||||
@@ -144,6 +150,7 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
|
||||
private bool HasSelectedUnit() => SelectedUnit is not null;
|
||||
private bool HasSelectedLesson() => SelectedLesson is not null;
|
||||
private bool CanAiAssist() => SelectedUnit is not null && _aiSettings.Enabled;
|
||||
|
||||
// ── Einheiten (4.1) ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -253,6 +260,15 @@ public partial class PlanningTabViewModel : ObservableObject
|
||||
if (result is not null) LoadUnits();
|
||||
}
|
||||
|
||||
/// KI-gestützte Planungsunterstützung (4.5.9) — die eigentliche Anfrage/Auswertung läuft im
|
||||
/// Dialog (<see cref="AiAssistDialogViewModel"/>), hier wird nur nachgeladen.
|
||||
[RelayCommand(CanExecute = nameof(CanAiAssist))]
|
||||
private async Task AiAssist()
|
||||
{
|
||||
if (OnAiAssist is null || SelectedUnit is null) return;
|
||||
if (await OnAiAssist(SelectedUnit.Model)) LoadUnits();
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(HasSelectedLesson))]
|
||||
private async Task EditLesson()
|
||||
{
|
||||
@@ -1067,6 +1083,91 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dialog: KI-gestützte Planungsunterstützung (4.5.9) ───────────────────────
|
||||
|
||||
/// Eine von der KI vorgeschlagene Stunde in der Prüfliste des Dialogs — angehakt = wird beim
|
||||
/// "Übernehmen" mit übertragen. <see cref="AiLesson.Id"/> unterscheidet neu/geändert (siehe
|
||||
/// AiPlanningDtos.cs), <see cref="IsNew"/> steuert hier nur die Anzeige ("Neu"/"Geändert").
|
||||
public partial class AiLessonReviewItem : ObservableObject
|
||||
{
|
||||
public AiLesson Source { get; }
|
||||
public bool IsNew { get; }
|
||||
public string DisplayLabel { get; }
|
||||
|
||||
[ObservableProperty] private bool _accepted = true;
|
||||
|
||||
public AiLessonReviewItem(AiLesson source, bool isNew)
|
||||
{
|
||||
Source = source;
|
||||
IsNew = isNew;
|
||||
var dateText = source.Date?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "kein Datum";
|
||||
DisplayLabel = isNew ? $"Neu: {source.Topic} ({dateText})" : $"Geändert: {source.Topic} ({dateText})";
|
||||
}
|
||||
}
|
||||
|
||||
public partial class AiAssistDialogViewModel : ObservableObject
|
||||
{
|
||||
private readonly AiPlanningService _aiPlanning;
|
||||
private readonly AiSettingsService _aiSettings;
|
||||
private readonly ILessonRepository _lessons;
|
||||
private readonly Unit _unit;
|
||||
|
||||
[ObservableProperty] private string _instruction = "";
|
||||
[ObservableProperty] private bool _isBusy;
|
||||
[ObservableProperty] private string _errorMessage = "";
|
||||
[ObservableProperty] private bool _hasResults;
|
||||
[ObservableProperty] private string? _summary;
|
||||
|
||||
public string UnitSummary { get; }
|
||||
public ObservableCollection<AiLessonReviewItem> ReviewItems { get; } = [];
|
||||
public bool Result { get; private set; }
|
||||
|
||||
public AiAssistDialogViewModel(AiPlanningService aiPlanning, AiSettingsService aiSettings,
|
||||
ILessonRepository lessons, Unit unit)
|
||||
{
|
||||
_aiPlanning = aiPlanning; _aiSettings = aiSettings; _lessons = lessons; _unit = unit;
|
||||
var lessonCount = lessons.GetByUnit(unit.Id).Count;
|
||||
UnitSummary = $"Einheit: {unit.Title} — {lessonCount} Stunde(n)";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task Send()
|
||||
{
|
||||
var token = _aiSettings.GetToken();
|
||||
if (token is null)
|
||||
{
|
||||
ErrorMessage = "Nicht angemeldet. Bitte in den Einstellungen bei der KI-Unterstützung anmelden.";
|
||||
return;
|
||||
}
|
||||
|
||||
ErrorMessage = ""; IsBusy = true;
|
||||
try
|
||||
{
|
||||
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token);
|
||||
var existingIds = _lessons.GetByUnit(_unit.Id).Select(l => l.Id).ToHashSet();
|
||||
|
||||
ReviewItems.Clear();
|
||||
foreach (var l in response.Lessons)
|
||||
ReviewItems.Add(new AiLessonReviewItem(l, isNew: l.Id is not { } id || !existingIds.Contains(id)));
|
||||
Summary = response.Summary;
|
||||
HasResults = true;
|
||||
}
|
||||
catch (AiBackendException ex) { ErrorMessage = ex.Message; }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Apply()
|
||||
{
|
||||
var accepted = ReviewItems.Where(i => i.Accepted).Select(i => i.Source).ToList();
|
||||
foreach (var lesson in _aiPlanning.ApplyResponse(_unit, accepted))
|
||||
_lessons.Save(lesson);
|
||||
Result = true;
|
||||
}
|
||||
|
||||
[RelayCommand] private void Cancel() => Result = false;
|
||||
}
|
||||
|
||||
// ── Dialog: Einheit als Vorlage in andere Gruppe kopieren (4.1.4) ────────────
|
||||
|
||||
public partial class CopyUnitDialogViewModel : ObservableObject
|
||||
|
||||
Reference in New Issue
Block a user