From 7b660c715267ad3583da46012faae4b82c7a48b0 Mon Sep 17 00:00:00 2001 From: Baddi86 Date: Mon, 14 Sep 2026 00:41:13 +0200 Subject: [PATCH] feat: Planungsideen-Feld je Stunde + persistierter Materialerstellungs-Prompt Lesson bekommt ein neues Freitextfeld PlanningIdeas fuer grobe Ideen vor der Feinplanung, sichtbar im LessonDialog und als Kontext an die KI-Planung (Backend + MCP) durchgereicht. LessonPhaseStep.MaterialPrompt speichert den beim Uebernehmen einer KI-Stunde erzeugten Materialerstellungs-Prompt dauerhaft, statt ihn nur einmalig im AiAssistDialog anzuzeigen - im Verlaufsplan-Editor ueber einen neuen Kopieren-Button je Phase erneut nutzbar, auch ueber MCP lesbar/schreibbar. Co-Authored-By: Claude Sonnet 5 --- LehrerApp.Core/AiPlanning/AiPlanningDtos.cs | 3 ++ LehrerApp.Core/Models/Planning.cs | 15 ++++++++ .../Services/AiPlanningService.cs | 9 +++++ LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs | 12 +++++-- .../Services/Mcp/Tools/LessonPlanTools.cs | 13 ++++--- .../ViewModels/Groups/PlanningViewModels.cs | 12 +++++++ .../Views/Groups/LessonDialog.axaml | 20 ++++++++--- .../Views/Groups/LessonDialog.axaml.cs | 15 ++++++++ TODO.md | 34 +++++++++++++++++++ ai-backend/explain.php | 3 +- ai-backend/plan.php | 11 ++++-- docs/Datenmodell.md | 9 +++++ 12 files changed, 141 insertions(+), 15 deletions(-) diff --git a/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs b/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs index 1b361c5..6a2a50d 100644 --- a/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs +++ b/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs @@ -76,6 +76,9 @@ public class AiLesson public List Phases { get; set; } = []; public string? Homework { get; set; } public string? Reflection { get; set; } + /// Rohentwurf/erste Ideen der Lehrkraft vor der Feinplanung (siehe Lesson.PlanningIdeas) — reiner + /// Kontext für die KI wie Homework/Reflection, wird symmetrisch übernommen/zurückgegeben. + public string? PlanningIdeas { get; set; } } public class AiPhaseStep diff --git a/LehrerApp.Core/Models/Planning.cs b/LehrerApp.Core/Models/Planning.cs index 16e050b..5bf9415 100644 --- a/LehrerApp.Core/Models/Planning.cs +++ b/LehrerApp.Core/Models/Planning.cs @@ -54,6 +54,13 @@ public class Lesson : IHasAttachments /// Optionaler Stundenbeginn — solange kein Stundenplan (Kapitel 4.3) existiert, manuell /// gepflegt. Dient nur der abgeleiteten Uhrzeit-Anzeige je Phase in . public TimeOnly? StartTime { get; set; } + /// Grobe Ideen/erster Entwurf, festgehalten lange bevor der Verlaufsplan () + /// feingeplant wird (Nutzer-Feedback: die eigentliche Ideenfindung liegt zeitlich oft weit vor + /// der Feinplanung) — bewusst ein eigenes Feld statt Zweckentfremdung von + /// (die ist nach der Stunde) oder (die ist auf Einheitenebene, nicht je + /// Stunde). Wird auch der KI-Planungsunterstützung (4.5.9) als Kontext mitgegeben, damit erste + /// eigene Ideen bei der KI-gestützten Weiterplanung nicht erneut abgetippt werden müssen. + public string? PlanningIdeas { get; set; } public List Phases { get; set; } = []; public TeachingTimelineState? TeachingTimeline { get; set; } public string? Homework { get; set; } @@ -95,6 +102,14 @@ public class LessonPhaseStep public string Activity { get; set; } = ""; public string Material { get; set; } = ""; public string Shorthand { get; set; } = ""; + /// Der über erzeugte, vollständige + /// Prompt für diese Phase (4.5.20), sofern die KI beim letzten "Übernehmen" einen Medienvorschlag + /// gemacht hatte — anders als der reine Vorschlagstext (AiPhaseStep.MaterialSuggestion, + /// nur transient während der Review) bewusst hier persistiert (Nutzer-Feedback: der Prompt soll + /// auch nach dem Übernehmen noch abrufbar/erneut kopierbar bleiben, statt nur einmalig im + /// Review-Dialog verfügbar zu sein). Rein informativ, kein Datenmodell-Bezug zu einem separaten + /// "Material"-Konzept, das es weiterhin nicht gibt — siehe TODO.md 4.5.26. + public string? MaterialPrompt { get; set; } /// /// null = Hauptweg. Sonst Verweis auf einen benannten aus /// dem Katalog, über den Phasen alternativer Unterrichtsverläufe zusammengehören — diff --git a/LehrerApp.Desktop/Services/AiPlanningService.cs b/LehrerApp.Desktop/Services/AiPlanningService.cs index dc8427a..48cc0ac 100644 --- a/LehrerApp.Desktop/Services/AiPlanningService.cs +++ b/LehrerApp.Desktop/Services/AiPlanningService.cs @@ -188,6 +188,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, StartTime = l.StartTime, Homework = l.Homework, Reflection = l.Reflection, + PlanningIdeas = l.PlanningIdeas, Phases = ToAiPhases(l.Phases, pathNames), }) .ToList(); @@ -270,6 +271,9 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, if (!string.Equals(existing.Reflection, proposed.Reflection, StringComparison.Ordinal)) diffs.Add("Reflexion geändert"); + if (!string.Equals(existing.PlanningIdeas, proposed.PlanningIdeas, StringComparison.Ordinal)) + diffs.Add("Planungsideen geändert"); + var pathNames = altPaths.GetAll().ToDictionary(p => p.Id, p => p.Name); var existingPhases = ToAiPhases(existing.Phases, pathNames); if (!PhasesEqual(existingPhases, proposed.Phases)) @@ -586,6 +590,7 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, StartTime = ai.StartTime, Homework = ai.Homework, Reflection = ai.Reflection, + PlanningIdeas = ai.PlanningIdeas, // Status bleibt bei einer Änderung erhalten — sonst würde eine bereits // durchgeführte Stunde durch eine KI-Anpassung stillschweigend auf "Geplant" // zurückgesetzt (die KI kennt/liefert diesen Status gar nicht). @@ -597,6 +602,10 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons, Activity = p.Activity, Material = p.Material, Shorthand = p.Shorthand, + // Persistiert den Prompt (4.5.20/4.5.36), damit er nach dem Übernehmen weiterhin + // im Stundeneditor kopierbar bleibt statt nur einmalig im Review-Dialog. + MaterialPrompt = string.IsNullOrWhiteSpace(p.MaterialSuggestion) + ? null : BuildMaterialPrompt(unit, ai, p), AlternativePathId = p.AlternativePathName is { } name && pathIdsByName.TryGetValue(name, out var pathId) ? pathId : null, }).ToList(), diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs b/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs index 623dfab..4d0a796 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/Dto.cs @@ -24,14 +24,20 @@ public record TimeEntryDto( Guid Id, Guid? TaskId, string Category, Guid? GroupId, DateOnly Date, TimeOnly? StartTime, TimeOnly? EndTime, int DurationMinutes, string? Description); -public record LessonPhaseDto(Guid Id, string Name, int DurationMinutes, string Activity, string Material, string Shorthand); +/// ist der beim letzten KI-"Übernehmen" gespeicherte +/// Materialerstellungs-Prompt (4.5.20/4.5.36) — nur gesetzt, wenn die KI für diese Phase einen +/// Medienvorschlag gemacht hatte. Ein MCP-Client kann ihn direkt zur Materialerzeugung nutzen, ohne +/// erst den Desktop-Dialog öffnen zu müssen. +public record LessonPhaseDto( + Guid Id, string Name, int DurationMinutes, string Activity, string Material, string Shorthand, + string? MaterialPrompt); public record LessonAttachmentDto(string StorageId, string FileName, long SizeBytes); public record LessonDto( Guid Id, Guid UnitId, Guid GroupId, DateOnly Date, int? LessonNumber, string Topic, - string? Homework, LessonStatus Status, List Competencies, List Phases, - List Attachments); + string? Homework, string? PlanningIdeas, LessonStatus Status, List Competencies, + List Phases, List Attachments); /// Ergebnis von "download_lesson_attachment": Inhalt Base64-kodiert, weil MCP-Tool-Antworten /// als JSON/Text übertragen werden. Bewusst kein Ressourcen-URI-Mechanismus (siehe Planungsdokument) - diff --git a/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs b/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs index d2ad82c..cb10c1b 100644 --- a/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs +++ b/LehrerApp.Desktop/Services/Mcp/Tools/LessonPlanTools.cs @@ -188,11 +188,12 @@ public class LessonPlanTools( return new WriteResultDto(true, lesson.Id, "Einzelstunde gespeichert."); } - [Description("Ändert Metadaten einer bestehenden Einzelstunde (Thema, Hausaufgabe, Status, Beginn, Stundennummer) — der Verlaufsplan (Phasen) bleibt unverändert. Nur angegebene Felder werden geändert.")] + [Description("Ändert Metadaten einer bestehenden Einzelstunde (Thema, Hausaufgabe, Planungsideen, Status, Beginn, Stundennummer) — der Verlaufsplan (Phasen) bleibt unverändert. Nur angegebene Felder werden geändert.")] public async Task UpdateLesson( [Description("ID der Einzelstunde.")] Guid lessonId, [Description("Neues Thema. Unverändert lassen: weglassen.")] string? topic = null, [Description("Neue Hausaufgabe. Unverändert lassen: weglassen.")] string? homework = null, + [Description("Neue Planungsideen (grober Entwurf vor der Feinplanung). Unverändert lassen: weglassen.")] string? planningIdeas = null, [Description("Neuer Status: Planned, Conducted, Draft, Ready oder Cancelled (ausgefallen, z.B. Exkursion/Feiertag - Alternative zu delete_lesson, wenn die Stunde als Ereignis dokumentiert bleiben soll). Unverändert lassen: weglassen.")] LessonStatus? status = null, [Description("Neuer Stundenbeginn, Format HH:mm. Unverändert lassen: weglassen.")] TimeOnly? startTime = null, [Description("Neue Stundennummer. Unverändert lassen: weglassen.")] int? lessonNumber = null, @@ -204,6 +205,7 @@ public class LessonPlanTools( var changes = new StringBuilder(); if (topic is not null && topic != lesson.Topic) { changes.AppendLine($"Thema: „{lesson.Topic}“ → „{topic}“"); lesson.Topic = topic; } if (homework is not null && homework != lesson.Homework) { changes.AppendLine($"Hausaufgabe: „{lesson.Homework}“ → „{homework}“"); lesson.Homework = homework; } + if (planningIdeas is not null && planningIdeas != lesson.PlanningIdeas) { changes.AppendLine($"Planungsideen: „{lesson.PlanningIdeas}“ → „{planningIdeas}“"); lesson.PlanningIdeas = planningIdeas; } if (status is not null && status != lesson.Status) { changes.AppendLine($"Status: {lesson.Status} → {status}"); lesson.Status = status.Value; } if (startTime is not null && startTime != lesson.StartTime) { changes.AppendLine($"Beginn: {lesson.StartTime:HH\\:mm} → {startTime:HH\\:mm}"); lesson.StartTime = startTime; } if (lessonNumber is not null && lessonNumber != lesson.LessonNumber) { changes.AppendLine($"Nr.: {lesson.LessonNumber} → {lessonNumber}"); lesson.LessonNumber = lessonNumber; } @@ -228,6 +230,7 @@ public class LessonPlanTools( [Description("Tätigkeit/Sozialform.")] string activity = "", [Description("Material.")] string material = "", [Description("Kurzsymbol, z.B. \"AB001->S\".")] string shorthand = "", + [Description("Optionaler, vollständiger Prompt zur Materialerstellung für diese Phase (siehe get_lesson_plans, LessonPhaseDto.MaterialPrompt) - z.B. wenn eine externe KI-Sitzung ihn selbst formuliert hat und er zur Wiederverwendung gespeichert werden soll.")] string? materialPrompt = null, CancellationToken ct = default) { var lesson = lessons.GetById(lessonId); @@ -240,7 +243,7 @@ public class LessonPlanTools( var phase = new LessonPhaseStep { Name = name, DurationMinutes = durationMinutes, Activity = activity, - Material = material, Shorthand = shorthand, + Material = material, Shorthand = shorthand, MaterialPrompt = materialPrompt, }; lesson.Phases.Add(phase); lessons.Save(lesson); @@ -256,6 +259,7 @@ public class LessonPlanTools( [Description("Neue Tätigkeit. Unverändert lassen: weglassen.")] string? activity = null, [Description("Neues Material. Unverändert lassen: weglassen.")] string? material = null, [Description("Neues Kurzsymbol. Unverändert lassen: weglassen.")] string? shorthand = null, + [Description("Neuer Prompt zur Materialerstellung (siehe get_lesson_plans, LessonPhaseDto.MaterialPrompt). Unverändert lassen: weglassen.")] string? materialPrompt = null, CancellationToken ct = default) { var lesson = lessons.GetById(lessonId); @@ -269,6 +273,7 @@ public class LessonPlanTools( if (activity is not null && activity != phase.Activity) { changes.AppendLine($"Tätigkeit: „{phase.Activity}“ → „{activity}“"); phase.Activity = activity; } if (material is not null && material != phase.Material) { changes.AppendLine($"Material: „{phase.Material}“ → „{material}“"); phase.Material = material; } if (shorthand is not null && shorthand != phase.Shorthand) { changes.AppendLine($"Kürzel: „{phase.Shorthand}“ → „{shorthand}“"); phase.Shorthand = shorthand; } + if (materialPrompt is not null && materialPrompt != phase.MaterialPrompt) { changes.AppendLine("Materialerstellungs-Prompt geändert."); phase.MaterialPrompt = materialPrompt; } if (changes.Length == 0) return new WriteResultDto(true, phase.Id, "Keine Änderung nötig."); @@ -409,7 +414,7 @@ public class LessonPlanTools( } private static LessonDto ToDto(Lesson l) => new( - l.Id, l.UnitId, l.GroupId, l.Date, l.LessonNumber, l.Topic, l.Homework, l.Status, l.Competencies, - l.Phases.Select(p => new LessonPhaseDto(p.Id, p.Name, p.DurationMinutes, p.Activity, p.Material, p.Shorthand)).ToList(), + l.Id, l.UnitId, l.GroupId, l.Date, l.LessonNumber, l.Topic, l.Homework, l.PlanningIdeas, l.Status, l.Competencies, + l.Phases.Select(p => new LessonPhaseDto(p.Id, p.Name, p.DurationMinutes, p.Activity, p.Material, p.Shorthand, p.MaterialPrompt)).ToList(), l.Attachments.Select(a => new LessonAttachmentDto(a.StorageId, a.FileName, a.SizeBytes)).ToList()); } diff --git a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs index 417f93b..b4a95fd 100644 --- a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs +++ b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs @@ -737,6 +737,7 @@ public partial class LessonDialogViewModel : ObservableObject [ObservableProperty] private int? _lessonNumber; [ObservableProperty] private string _topic = ""; [ObservableProperty] private string _startTimeText = ""; + [ObservableProperty] private string _planningIdeas = ""; [ObservableProperty] private string _homework = ""; [ObservableProperty] private bool _homeworkChecked; [ObservableProperty] private bool _homeworkCheckDismissed; @@ -823,6 +824,7 @@ public partial class LessonDialogViewModel : ObservableObject LessonNumber = editingLesson.LessonNumber; Topic = editingLesson.Topic; StartTimeText = editingLesson.StartTime?.ToString("HH:mm") ?? ""; + PlanningIdeas = editingLesson.PlanningIdeas ?? ""; Homework = editingLesson.Homework ?? ""; HomeworkChecked = editingLesson.HomeworkChecked; HomeworkCheckDismissed = editingLesson.HomeworkCheckDismissed; @@ -888,6 +890,7 @@ public partial class LessonDialogViewModel : ObservableObject Activity = source?.Activity ?? "", Material = source?.Material ?? "", Shorthand = source?.Shorthand ?? "", + MaterialPrompt = source?.MaterialPrompt, }; item.OnChanged = RecomputeTimes; item.OnRemove = RemovePhase; @@ -1075,6 +1078,7 @@ public partial class LessonDialogViewModel : ObservableObject Result.LessonNumber = LessonNumber; Result.Topic = Topic.Trim(); Result.StartTime = startTime; + Result.PlanningIdeas = string.IsNullOrWhiteSpace(PlanningIdeas) ? null : PlanningIdeas.Trim(); Result.Phases = Phases.Select(p => p.ToModel()).ToList(); Result.Homework = string.IsNullOrWhiteSpace(Homework) ? null : Homework.Trim(); Result.HomeworkChecked = HomeworkChecked; @@ -1103,6 +1107,13 @@ public partial class PhaseStepEditItem : ObservableObject [ObservableProperty] private string _material = ""; [ObservableProperty] private string _shorthand = ""; [ObservableProperty] private string _computedTimeDisplay = ""; + /// Gespeicherter Materialerstellungs-Prompt (4.5.20/4.5.36) — nur gesetzt, wenn die KI beim + /// letzten "Übernehmen" einen Medienvorschlag für diese Phase gemacht hatte. Steuert die + /// Sichtbarkeit des Kopieren-Buttons im Verlaufsplan-Editor. + [ObservableProperty] private string? _materialPrompt; + + public bool HasMaterialPrompt => !string.IsNullOrWhiteSpace(MaterialPrompt); + partial void OnMaterialPromptChanged(string? value) => OnPropertyChanged(nameof(HasMaterialPrompt)); /// Checkbox-Zustand im Editor: unchecked→checked öffnet den Zuweisen-Dialog /// (); checked→unchecked entfernt die Zuordnung. @@ -1162,6 +1173,7 @@ public partial class PhaseStepEditItem : ObservableObject Activity = Activity.Trim(), Material = Material.Trim(), Shorthand = Shorthand.Trim(), + MaterialPrompt = MaterialPrompt, AlternativePathId = AlternativePathId, }; } diff --git a/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml b/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml index 78388c7..2645092 100644 --- a/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml +++ b/LehrerApp.Desktop/Views/Groups/LessonDialog.axaml @@ -49,6 +49,13 @@ IsVisible="{Binding TopicError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> + + + + + @@ -66,7 +73,7 @@ - + @@ -83,7 +90,7 @@ - + -