KI-Feature 4.5.22: KI-Unterstützung direkt aus dem Stunden-Editor starten
Bisher musste man aus dem Stunden-Editor raus, die KI-Hilfe für die ganze Einheit aufrufen und die gewünschte Stunde in der freien Anweisung erst benennen. Neuer Button "KI-Unterstützung für diese Stunde" im LessonDialog öffnet denselben AiAssistDialog im neuen Fokus-Modus. AiPlanningRequest.FocusLessonId weist die KI im Systemprompt an, sich auf genau diese Stunde zu beschränken; zusätzlich wie beim Umfangs-Umschalter client-seitig hart durchgesetzt in Send() und ApplyResponse, statt der KI-Antwort blind zu vertrauen. Da "Übernehmen" direkt ins Repository speichert, wären die noch offenen Feldwerte des ursprünglichen LessonDialog danach veraltet gewesen - der Dialog schließt sich deshalb nach einer angewendeten KI-Änderung automatisch mit dem frischen Stand statt ihn mit alten Werten zu überschreiben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,12 @@ public class AiPlanningRequest
|
|||||||
// Systemprompt des Backends durchgesetzt als auch client-seitig in ApplyResponse defensiv
|
// Systemprompt des Backends durchgesetzt als auch client-seitig in ApplyResponse defensiv
|
||||||
// geprüft — die KI-Antwort wird dafür nicht blind vertraut.
|
// geprüft — die KI-Antwort wird dafür nicht blind vertraut.
|
||||||
public bool AllowModifyingExistingLessons { get; set; } = true;
|
public bool AllowModifyingExistingLessons { get; set; } = true;
|
||||||
|
// Gesetzt, wenn die Anfrage aus dem Editor einer einzelnen Stunde heraus gestartet wurde
|
||||||
|
// (statt aus der Einheiten-Übersicht): die KI soll dann AUSSCHLIESSLICH diese eine Stunde
|
||||||
|
// bearbeiten, keine neuen Stunden vorschlagen und keine andere bestehende Stunde anfassen.
|
||||||
|
// Wie AllowModifyingExistingLessons sowohl im Systemprompt erbeten als auch client-seitig
|
||||||
|
// hart durchgesetzt (siehe AiAssistDialogViewModel.Send/AiPlanningService.ApplyResponse).
|
||||||
|
public Guid? FocusLessonId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AiUnitContext
|
public class AiUnitContext
|
||||||
|
|||||||
@@ -317,6 +317,36 @@ public sealed class AiPlanningServiceTests
|
|||||||
Assert.Equal("Neue Stunde bleibt erlaubt", lesson.Topic);
|
Assert.Equal("Neue Stunde bleibt erlaubt", lesson.Topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fokus-Modus (4.5.22, KI-Unterstützung aus dem Stunden-Editor heraus): auch wenn die KI die
|
||||||
|
/// Anweisung ignoriert und weitere/andere Stunden zurückgibt, darf clientseitig nur die
|
||||||
|
/// angefragte Fokus-Stunde übernommen werden.
|
||||||
|
[Fact]
|
||||||
|
public void ApplyResponse_FocusLessonId_VerwirftAlleAnderenLessons()
|
||||||
|
{
|
||||||
|
var group = new LearningGroup();
|
||||||
|
var unit = new Unit { GroupId = group.Id, Title = "T" };
|
||||||
|
var focus = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Fokus-Stunde" };
|
||||||
|
var other = new Lesson { UnitId = unit.Id, GroupId = group.Id, Topic = "Andere Stunde" };
|
||||||
|
var lessons = new FakeLessons();
|
||||||
|
lessons.Add(focus);
|
||||||
|
lessons.Add(other);
|
||||||
|
|
||||||
|
var service = Build(lessons, new FakeGroups([group]), new FakeSubjects([]),
|
||||||
|
new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
|
||||||
|
|
||||||
|
var accepted = new List<AiLesson>
|
||||||
|
{
|
||||||
|
new() { Id = focus.Id, Topic = "Fokus-Stunde, geändert" },
|
||||||
|
new() { Id = other.Id, Topic = "Sollte verworfen werden" },
|
||||||
|
new() { Id = null, Topic = "Neue Stunde sollte verworfen werden" },
|
||||||
|
};
|
||||||
|
var result = service.ApplyResponse(unit, accepted, focusLessonId: focus.Id);
|
||||||
|
|
||||||
|
var lesson = Assert.Single(result);
|
||||||
|
Assert.Equal(focus.Id, lesson.Id);
|
||||||
|
Assert.Equal("Fokus-Stunde, geändert", lesson.Topic);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ApplyResponse_NullId_WirdAlsNeueLessonBehandelt()
|
public void ApplyResponse_NullId_WirdAlsNeueLessonBehandelt()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -299,13 +299,14 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AiPlanningResponse> RequestPlanAsync(Unit unit, string instruction, string token,
|
public async Task<AiPlanningResponse> RequestPlanAsync(Unit unit, string instruction, string token,
|
||||||
bool allowModifyingExistingLessons = true, List<AiLesson>? draftOverrides = null)
|
bool allowModifyingExistingLessons = true, List<AiLesson>? draftOverrides = null, Guid? focusLessonId = null)
|
||||||
{
|
{
|
||||||
var request = new AiPlanningRequest
|
var request = new AiPlanningRequest
|
||||||
{
|
{
|
||||||
Instruction = instruction,
|
Instruction = instruction,
|
||||||
Unit = BuildContext(unit, instruction, draftOverrides),
|
Unit = BuildContext(unit, instruction, draftOverrides),
|
||||||
AllowModifyingExistingLessons = allowModifyingExistingLessons,
|
AllowModifyingExistingLessons = allowModifyingExistingLessons,
|
||||||
|
FocusLessonId = focusLessonId,
|
||||||
};
|
};
|
||||||
|
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Post, "plan.php")
|
using var req = new HttpRequestMessage(HttpMethod.Post, "plan.php")
|
||||||
@@ -388,9 +389,12 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
|||||||
/// interpretiert, sondern immer als neue Lesson behandelt (Anti-Halluzinations-Absicherung).
|
/// interpretiert, sondern immer als neue Lesson behandelt (Anti-Halluzinations-Absicherung).
|
||||||
/// Ist <paramref name="allowModifyingExistingLessons"/> false, werden Änderungen an bestehenden
|
/// Ist <paramref name="allowModifyingExistingLessons"/> false, werden Änderungen an bestehenden
|
||||||
/// Lessons zusätzlich hart verworfen (nicht nur per Systemprompt an die KI erbeten) — die
|
/// Lessons zusätzlich hart verworfen (nicht nur per Systemprompt an die KI erbeten) — die
|
||||||
/// Einschränkung wird also nicht blind der KI-Antwort überlassen.
|
/// Einschränkung wird also nicht blind der KI-Antwort überlassen. Ist
|
||||||
|
/// <paramref name="focusLessonId"/> gesetzt (Anfrage aus dem Editor einer einzelnen Stunde
|
||||||
|
/// heraus), wird ebenso hart jede Lesson mit abweichender oder fehlender Id verworfen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<Lesson> ApplyResponse(Unit unit, List<AiLesson> acceptedLessons, bool allowModifyingExistingLessons = true)
|
public List<Lesson> ApplyResponse(Unit unit, List<AiLesson> acceptedLessons,
|
||||||
|
bool allowModifyingExistingLessons = true, Guid? focusLessonId = null)
|
||||||
{
|
{
|
||||||
var pathIdsByName = altPaths.GetAll().ToDictionary(p => p.Name, p => p.Id);
|
var pathIdsByName = altPaths.GetAll().ToDictionary(p => p.Name, p => p.Id);
|
||||||
var existingLessons = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
|
var existingLessons = lessons.GetByUnit(unit.Id).ToDictionary(l => l.Id);
|
||||||
@@ -398,6 +402,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
|||||||
var result = new List<Lesson>();
|
var result = new List<Lesson>();
|
||||||
foreach (var ai in acceptedLessons)
|
foreach (var ai in acceptedLessons)
|
||||||
{
|
{
|
||||||
|
if (focusLessonId is { } focus && ai.Id != focus) continue;
|
||||||
var isUpdate = ai.Id is { } id && existingLessons.ContainsKey(id);
|
var isUpdate = ai.Id is { } id && existingLessons.ContainsKey(id);
|
||||||
if (isUpdate && !allowModifyingExistingLessons) continue;
|
if (isUpdate && !allowModifyingExistingLessons) continue;
|
||||||
result.Add(new Lesson
|
result.Add(new Lesson
|
||||||
|
|||||||
@@ -631,6 +631,18 @@ public partial class LessonDialogViewModel : ObservableObject
|
|||||||
public string DialogTitle => _editingLesson is null ? "Neue Stunde anlegen" : "Stunde bearbeiten";
|
public string DialogTitle => _editingLesson is null ? "Neue Stunde anlegen" : "Stunde bearbeiten";
|
||||||
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
public string SaveButtonText => _editingLesson is null ? "Anlegen" : "Speichern";
|
||||||
|
|
||||||
|
// Für die KI-Unterstützung mit Fokus auf genau diese Stunde (4.5.22) — nur für bereits
|
||||||
|
// gespeicherte Stunden sinnvoll, eine gerade erst angelegte, noch ungespeicherte Stunde hat
|
||||||
|
// keine echte Id, auf die sich die KI beziehen könnte.
|
||||||
|
public Guid UnitId => _unitId;
|
||||||
|
public Lesson? EditingLesson => _editingLesson;
|
||||||
|
public bool CanAiAssist => _editingLesson is not null;
|
||||||
|
|
||||||
|
/// Vom Code-Behind nach einer über die KI angewendeten Änderung aufgerufen: der Dialog schließt
|
||||||
|
/// sich danach mit Result != null, damit die aufrufende Liste neu lädt — die eigenen, jetzt
|
||||||
|
/// veralteten Feldwerte dieses Fensters werden NICHT mehr über die KI-Änderung gespeichert.
|
||||||
|
public void MarkAppliedExternally(Lesson updated) => Result = updated;
|
||||||
|
|
||||||
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
public LessonDialogViewModel(ILessonRepository lessons, IShorthandCodeRepository shorthandCodes,
|
||||||
IAlternativeLessonPathRepository alternativePaths, ITimetableSlotRepository timetableSlots,
|
IAlternativeLessonPathRepository alternativePaths, ITimetableSlotRepository timetableSlots,
|
||||||
PeriodScheduleService periodSchedule, Guid unitId, Guid groupId,
|
PeriodScheduleService periodSchedule, Guid unitId, Guid groupId,
|
||||||
@@ -1156,6 +1168,7 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
|||||||
private readonly AiSettingsService _aiSettings;
|
private readonly AiSettingsService _aiSettings;
|
||||||
private readonly ILessonRepository _lessons;
|
private readonly ILessonRepository _lessons;
|
||||||
private readonly Unit _unit;
|
private readonly Unit _unit;
|
||||||
|
private readonly Lesson? _focusLesson;
|
||||||
|
|
||||||
[ObservableProperty] private string _instruction = "";
|
[ObservableProperty] private string _instruction = "";
|
||||||
[ObservableProperty] private bool _allowModifyingExisting = true;
|
[ObservableProperty] private bool _allowModifyingExisting = true;
|
||||||
@@ -1168,10 +1181,22 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
|||||||
public ObservableCollection<AiLessonReviewItem> ReviewItems { get; } = [];
|
public ObservableCollection<AiLessonReviewItem> ReviewItems { get; } = [];
|
||||||
public bool Result { get; private set; }
|
public bool Result { get; private set; }
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// Stunde bearbeiten, der Umfangs-Umschalter macht in diesem Modus keinen Sinn und wird
|
||||||
|
/// ausgeblendet (siehe AiAssistDialog.axaml).
|
||||||
|
public bool IsFocusMode => _focusLesson is not null;
|
||||||
|
public string? FocusLabel => _focusLesson is null ? null
|
||||||
|
: $"Fokus: nur diese Stunde — „{_focusLesson.Topic}“ ({_focusLesson.Date:dd.MM.yyyy})";
|
||||||
|
|
||||||
public AiAssistDialogViewModel(AiPlanningService aiPlanning, AiSettingsService aiSettings,
|
public AiAssistDialogViewModel(AiPlanningService aiPlanning, AiSettingsService aiSettings,
|
||||||
ILessonRepository lessons, Unit unit)
|
ILessonRepository lessons, Unit unit, Lesson? focusLesson = null)
|
||||||
{
|
{
|
||||||
_aiPlanning = aiPlanning; _aiSettings = aiSettings; _lessons = lessons; _unit = unit;
|
_aiPlanning = aiPlanning; _aiSettings = aiSettings; _lessons = lessons; _unit = unit;
|
||||||
|
_focusLesson = focusLesson;
|
||||||
|
// Ohne Änderungserlaubnis gäbe es im Fokus-Modus nichts, was die KI überhaupt vorschlagen
|
||||||
|
// dürfte (neue Stunden sind hier ja ausdrücklich nicht das Ziel) — deshalb erzwungen an.
|
||||||
|
if (IsFocusMode) AllowModifyingExisting = true;
|
||||||
var lessonCount = lessons.GetByUnit(unit.Id).Count;
|
var lessonCount = lessons.GetByUnit(unit.Id).Count;
|
||||||
UnitSummary = $"Einheit: {unit.Title} — {lessonCount} Stunde(n)";
|
UnitSummary = $"Einheit: {unit.Title} — {lessonCount} Stunde(n)";
|
||||||
}
|
}
|
||||||
@@ -1196,11 +1221,18 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
|||||||
ErrorMessage = ""; IsBusy = true;
|
ErrorMessage = ""; IsBusy = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token, AllowModifyingExisting, draftOverrides);
|
var response = await _aiPlanning.RequestPlanAsync(_unit, Instruction, token, AllowModifyingExisting,
|
||||||
|
draftOverrides, _focusLesson?.Id);
|
||||||
var existingLessons = _lessons.GetByUnit(_unit.Id).ToDictionary(l => l.Id);
|
var existingLessons = _lessons.GetByUnit(_unit.Id).ToDictionary(l => l.Id);
|
||||||
|
|
||||||
|
// Im Fokus-Modus hart auf die eine angefragte Stunde beschränken, statt der KI-Antwort
|
||||||
|
// zu vertrauen — dieselbe Absicherung wie beim Umfangs-Umschalter unten.
|
||||||
|
var lessonsToShow = IsFocusMode
|
||||||
|
? response.Lessons.Where(l => l.Id == _focusLesson!.Id).ToList()
|
||||||
|
: response.Lessons;
|
||||||
|
|
||||||
ReviewItems.Clear();
|
ReviewItems.Clear();
|
||||||
foreach (var l in response.Lessons)
|
foreach (var l in lessonsToShow)
|
||||||
{
|
{
|
||||||
var isExisting = l.Id is { } id && existingLessons.ContainsKey(id);
|
var isExisting = l.Id is { } id && existingLessons.ContainsKey(id);
|
||||||
// Falls der Modus Änderungen an bestehenden Stunden verbietet, aber die KI die
|
// Falls der Modus Änderungen an bestehenden Stunden verbietet, aber die KI die
|
||||||
@@ -1226,7 +1258,7 @@ public partial class AiAssistDialogViewModel : ObservableObject
|
|||||||
private void Apply()
|
private void Apply()
|
||||||
{
|
{
|
||||||
var accepted = ReviewItems.Where(i => i.Accepted).Select(i => i.Source).ToList();
|
var accepted = ReviewItems.Where(i => i.Accepted).Select(i => i.Source).ToList();
|
||||||
foreach (var lesson in _aiPlanning.ApplyResponse(_unit, accepted, AllowModifyingExisting))
|
foreach (var lesson in _aiPlanning.ApplyResponse(_unit, accepted, AllowModifyingExisting, _focusLesson?.Id))
|
||||||
_lessons.Save(lesson);
|
_lessons.Save(lesson);
|
||||||
Result = true;
|
Result = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
<StackPanel Spacing="14" Margin="0,0,12,0">
|
<StackPanel Spacing="14" Margin="0,0,12,0">
|
||||||
<TextBlock Text="KI-Unterstützung" Classes="dialogtitle"/>
|
<TextBlock Text="KI-Unterstützung" Classes="dialogtitle"/>
|
||||||
<TextBlock Text="{Binding UnitSummary}" FontSize="12" Opacity="0.6"/>
|
<TextBlock Text="{Binding UnitSummary}" FontSize="12" Opacity="0.6"/>
|
||||||
|
<TextBlock Text="{Binding FocusLabel}" FontSize="12" FontWeight="SemiBold"
|
||||||
|
IsVisible="{Binding IsFocusMode}"/>
|
||||||
|
|
||||||
<StackPanel Spacing="10" IsVisible="{Binding HasResults}">
|
<StackPanel Spacing="10" IsVisible="{Binding HasResults}">
|
||||||
<TextBlock Text="{Binding Summary}" FontSize="12" TextWrapping="Wrap"
|
<TextBlock Text="{Binding Summary}" FontSize="12" TextWrapping="Wrap"
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
PlaceholderText="z.B. Ergänze zwei weitere Stunden zum Thema Redoxreaktionen mit steigendem Anspruch."
|
PlaceholderText="z.B. Ergänze zwei weitere Stunden zum Thema Redoxreaktionen mit steigendem Anspruch."
|
||||||
IsEnabled="{Binding !IsBusy}"/>
|
IsEnabled="{Binding !IsBusy}"/>
|
||||||
<CheckBox Content="Auch bestehende Stundeninhalte anpassen" IsChecked="{Binding AllowModifyingExisting}"
|
<CheckBox Content="Auch bestehende Stundeninhalte anpassen" IsChecked="{Binding AllowModifyingExisting}"
|
||||||
IsEnabled="{Binding !IsBusy}"
|
IsEnabled="{Binding !IsBusy}" IsVisible="{Binding !IsFocusMode}"
|
||||||
ToolTip.Tip="Deaktivieren, um die Einheit nur um neue Stunden zu erweitern, ohne den Inhalt bereits vorhandener Stunden zu verändern."/>
|
ToolTip.Tip="Deaktivieren, um die Einheit nur um neue Stunden zu erweitern, ohne den Inhalt bereits vorhandener Stunden zu verändern."/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|||||||
@@ -133,9 +133,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</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="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||||
<Button Grid.Column="2" Content="{Binding SaveButtonText}"
|
<Button Grid.Column="2" Content="🤖 KI-Unterstützung für diese Stunde" HorizontalAlignment="Stretch"
|
||||||
|
Click="OnAiAssist" IsVisible="{Binding CanAiAssist}"
|
||||||
|
ToolTip.Tip="Fragt die KI gezielt zu genau dieser Stunde — Speicherstand geht dabei direkt in die Datenbank, nicht über die Felder hier."/>
|
||||||
|
<Button Grid.Column="4" Content="{Binding SaveButtonText}"
|
||||||
HorizontalAlignment="Stretch" Click="OnSave"/>
|
HorizontalAlignment="Stretch" Click="OnSave"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Avalonia.Controls;
|
|||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using LehrerApp.Core.Interfaces;
|
using LehrerApp.Core.Interfaces;
|
||||||
using LehrerApp.Core.Models;
|
using LehrerApp.Core.Models;
|
||||||
|
using LehrerApp.Desktop.Services;
|
||||||
using LehrerApp.Desktop.ViewModels.Groups;
|
using LehrerApp.Desktop.ViewModels.Groups;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@@ -38,4 +39,34 @@ public partial class LessonDialog : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
private void OnCancel(object? s, RoutedEventArgs e) => Close(false);
|
||||||
|
|
||||||
|
/// KI-Unterstützung mit Fokus auf genau diese (bereits gespeicherte) Stunde, statt den Umweg
|
||||||
|
/// über die Einheiten-Übersicht nehmen zu müssen (4.5.22, Nutzer-Feedback). Die Übernahme im
|
||||||
|
/// AiAssistDialog speichert direkt ins Repository — dieser Dialog schließt sich danach mit dem
|
||||||
|
/// frisch geladenen Stand, statt seine eigenen (jetzt veralteten) Feldwerte zu speichern.
|
||||||
|
private async void OnAiAssist(object? s, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is not LessonDialogViewModel vm || vm.EditingLesson is null) return;
|
||||||
|
|
||||||
|
var unitRepo = App.Services.GetRequiredService<IUnitRepository>();
|
||||||
|
var unit = unitRepo.GetById(vm.UnitId);
|
||||||
|
if (unit is null) return;
|
||||||
|
|
||||||
|
var lessonRepo = App.Services.GetRequiredService<ILessonRepository>();
|
||||||
|
var currentLesson = lessonRepo.GetByUnit(vm.UnitId).FirstOrDefault(l => l.Id == vm.EditingLesson.Id);
|
||||||
|
if (currentLesson is null) return;
|
||||||
|
|
||||||
|
var dialogVm = new AiAssistDialogViewModel(
|
||||||
|
App.Services.GetRequiredService<AiPlanningService>(),
|
||||||
|
App.Services.GetRequiredService<AiSettingsService>(),
|
||||||
|
lessonRepo, unit, focusLesson: currentLesson);
|
||||||
|
|
||||||
|
var dialog = new AiAssistDialog { DataContext = dialogVm };
|
||||||
|
var ok = await dialog.ShowDialog<bool>(this);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
var updated = lessonRepo.GetByUnit(vm.UnitId).FirstOrDefault(l => l.Id == vm.EditingLesson.Id);
|
||||||
|
if (updated is not null) vm.MarkAppliedExternally(updated);
|
||||||
|
Close(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -840,6 +840,27 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
|||||||
(`AiLessonReviewItem.RequestExplanationCommand`), der nach Laden durch den Text ersetzt
|
(`AiLessonReviewItem.RequestExplanationCommand`), der nach Laden durch den Text ersetzt
|
||||||
wird — dieselbe Anfrage wird nicht zweimal angeboten. Kein neues DB-Schema nötig (nutzt
|
wird — dieselbe Anfrage wird nicht zweimal angeboten. Kein neues DB-Schema nötig (nutzt
|
||||||
dieselben `users`/`tokens`/`transactions`-Tabellen wie `plan.php`).
|
dieselben `users`/`tokens`/`transactions`-Tabellen wie `plan.php`).
|
||||||
|
- [x] **4.5.22** KI-Unterstützung direkt aus dem Editor einer einzelnen Stunde heraus statt nur aus
|
||||||
|
der Einheiten-Übersicht (Nutzer-Feedback: bisher musste man aus der Stunde raus, die
|
||||||
|
Einheiten-KI aufrufen und die gewünschte Stunde in der freien Anweisung erst benennen).
|
||||||
|
**Umsetzung:** neuer "Fokus-Modus" im `AiAssistDialog` statt eines separaten Dialogs — Button
|
||||||
|
"🤖 KI-Unterstützung für diese Stunde" im `LessonDialog` (nur bei bereits gespeicherten
|
||||||
|
Stunden, eine gerade erst angelegte hat noch keine Id zum Fokussieren) öffnet denselben
|
||||||
|
`AiAssistDialog`, aber mit `focusLesson` gesetzt. Neues Wire-Feld
|
||||||
|
`AiPlanningRequest.FocusLessonId`: der Systemprompt in `plan.php` weist die KI an,
|
||||||
|
ausschließlich diese eine Stunde zu bearbeiten, keine neuen Stunden vorzuschlagen und keine
|
||||||
|
andere Stunde anzufassen — zusätzlich (wie beim Umfangs-Umschalter 4.5.15) client-seitig hart
|
||||||
|
durchgesetzt in `AiAssistDialogViewModel.Send` (Antwort wird auf die Fokus-Id gefiltert,
|
||||||
|
bevor sie überhaupt angezeigt wird) und `AiPlanningService.ApplyResponse`. Der
|
||||||
|
Umfangs-Umschalter ("Auch bestehende Stundeninhalte anpassen") ergibt im Fokus-Modus keinen
|
||||||
|
Sinn und wird ausgeblendet (immer erzwungen an).
|
||||||
|
**Datenkonsistenz-Falle vermieden:** Da `AiAssistDialog` bei "Übernehmen" direkt ins
|
||||||
|
Repository speichert, wären die noch offenen Feldwerte des ursprünglichen `LessonDialog`
|
||||||
|
danach veraltet — ein anschließendes normales "Speichern" hätte die KI-Änderung
|
||||||
|
stillschweigend wieder überschrieben. Der `LessonDialog` schließt sich deshalb nach einer
|
||||||
|
angewendeten KI-Änderung automatisch (`LessonDialogViewModel.MarkAppliedExternally`, lädt den
|
||||||
|
frischen Stand statt der eigenen Felder), die aufrufende Liste aktualisiert sich wie nach
|
||||||
|
einem normalen Speichern.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Die Eingabe hat exakt diese Struktur:
|
|||||||
{
|
{
|
||||||
"instruction": "<freie Anweisung der Lehrkraft, kann auch leer sein>",
|
"instruction": "<freie Anweisung der Lehrkraft, kann auch leer sein>",
|
||||||
"allowModifyingExistingLessons": <true oder false, siehe Abschnitt "Umfang dieser Anfrage">,
|
"allowModifyingExistingLessons": <true oder false, siehe Abschnitt "Umfang dieser Anfrage">,
|
||||||
|
"focusLessonId": "<GUID einer einzelnen Stunde oder null, siehe Abschnitt "Umfang dieser Anfrage">,
|
||||||
"unit": {
|
"unit": {
|
||||||
"id": "<GUID der Einheit>",
|
"id": "<GUID der Einheit>",
|
||||||
"title": "<Titel der Einheit>",
|
"title": "<Titel der Einheit>",
|
||||||
@@ -86,6 +87,12 @@ Das Feld "allowModifyingExistingLessons" in der Eingabe legt fest, was du vorsch
|
|||||||
verlangt, die dadurch nicht möglich ist, erkläre das kurz im "summary"-Feld und schlage
|
verlangt, die dadurch nicht möglich ist, erkläre das kurz im "summary"-Feld und schlage
|
||||||
stattdessen sinnvolle neue Stunden vor.
|
stattdessen sinnvolle neue Stunden vor.
|
||||||
|
|
||||||
|
Ist "focusLessonId" gesetzt (Anfrage aus dem Editor einer einzelnen Stunde heraus, nicht aus der
|
||||||
|
Einheiten-Übersicht): Bearbeite AUSSCHLIESSLICH die Stunde mit genau dieser Id gemäß der
|
||||||
|
Anweisung. Schlage KEINE neuen Stunden vor und ändere KEINE andere bestehende Stunde, auch wenn
|
||||||
|
"allowModifyingExistingLessons" true ist — die Lehrkraft sieht in diesem Fall nur diese eine
|
||||||
|
Stunde zur Prüfung, alles andere würde ihr gar nicht angezeigt.
|
||||||
|
|
||||||
## Fachlicher Kontext und Konventionen
|
## Fachlicher Kontext und Konventionen
|
||||||
|
|
||||||
- Richte Anspruch, Wortwahl und Methodik nach "subjectName"/"gradeLevel"/"groupName" aus.
|
- Richte Anspruch, Wortwahl und Methodik nach "subjectName"/"gradeLevel"/"groupName" aus.
|
||||||
|
|||||||
Reference in New Issue
Block a user