diff --git a/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs b/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs
index 5ab9871..810f456 100644
--- a/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs
+++ b/LehrerApp.Core/AiPlanning/AiPlanningDtos.cs
@@ -82,6 +82,12 @@ public class AiPhaseStep
// Name statt Guid: LLMs erfinden/verändern Guids unzuverlässig, ein Name ist beim Import
// gegen den Katalog abgleichbar (kein Treffer -> null = Hauptweg, kein Fehler).
public string? AlternativePathName { get; set; }
+ // Kurzer Vorschlag für ein konkretes Medium/Material zu dieser Phase (z.B. Tafelbild,
+ // Arbeitsblatt), nur gesetzt, wenn eines wirklich sinnvoll wäre — nicht bei jeder Phase.
+ // Wird NICHT in Lesson.Phases übernommen (nur für die Review-Anzeige/Prompt-Erzeugung
+ // relevant, siehe AiPlanningService.BuildMaterialPrompt), daher kein Feld auf
+ // LessonPhaseStep nötig.
+ public string? MaterialSuggestion { get; set; }
}
public class AiPlanningResponse
diff --git a/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs b/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs
index 2911113..191ff92 100644
--- a/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs
+++ b/LehrerApp.Desktop.Tests/AiPlanningServiceTests.cs
@@ -205,6 +205,50 @@ public sealed class AiPlanningServiceTests
Assert.Contains(service.DescribeChanges(existing, proposed), d => d.Contains("Verlaufsplan"));
}
+ [Fact]
+ public void BuildMaterialPrompt_EnthaeltKontextUndVorschlag()
+ {
+ var subject = new Subject { Name = "Chemie" };
+ var group = new LearningGroup { Name = "9c", SubjectId = subject.Id, GradeLevel = 9 };
+ var unit = new Unit { GroupId = group.Id, Title = "Redoxreaktionen" };
+ var lesson = new AiLesson { Topic = "Elektrolyse", Date = new DateOnly(2026, 3, 17), Phases = [] };
+ var phase = new AiPhaseStep
+ {
+ Name = "Erarbeitung", DurationMinutes = 20, Activity = "Experiment auswerten",
+ Material = "AB003", MaterialSuggestion = "Tafelbild mit dem Aufbau der Elektrolysezelle.",
+ };
+
+ var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([subject]),
+ new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
+
+ var prompt = service.BuildMaterialPrompt(unit, lesson, phase);
+
+ Assert.Contains("Chemie", prompt);
+ Assert.Contains("9c", prompt);
+ Assert.Contains("Redoxreaktionen", prompt);
+ Assert.Contains("Elektrolyse", prompt);
+ Assert.Contains("Erarbeitung", prompt);
+ Assert.Contains("AB003", prompt);
+ Assert.Contains("Tafelbild mit dem Aufbau der Elektrolysezelle.", prompt);
+ }
+
+ [Fact]
+ public void BuildMaterialPrompt_OhneBisherigesMaterial_LaesstZeileWeg()
+ {
+ var group = new LearningGroup();
+ var unit = new Unit { GroupId = group.Id, Title = "T" };
+ var lesson = new AiLesson { Topic = "Stunde", Phases = [] };
+ var phase = new AiPhaseStep { Name = "Einstieg", MaterialSuggestion = "Kurzer Impuls per Bild." };
+
+ var service = Build(new FakeLessons(), new FakeGroups([group]), new FakeSubjects([]),
+ new FakeCompetencyDomains(), new FakeAlternativeLessonPaths([]));
+
+ var prompt = service.BuildMaterialPrompt(unit, lesson, phase);
+
+ Assert.DoesNotContain("Bisheriges Material", prompt);
+ Assert.Contains("Kurzer Impuls per Bild.", prompt);
+ }
+
[Fact]
public void ApplyResponse_BekannteId_WirdAlsUpdateBehandelt()
{
diff --git a/LehrerApp.Desktop/Services/AiPlanningService.cs b/LehrerApp.Desktop/Services/AiPlanningService.cs
index 9ac93d9..e96f8fb 100644
--- a/LehrerApp.Desktop/Services/AiPlanningService.cs
+++ b/LehrerApp.Desktop/Services/AiPlanningService.cs
@@ -249,6 +249,39 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
return diffs;
}
+ ///
+ /// Baut aus Einheit/Stunde/Phase und dem KI-Medienvorschlag ()
+ /// einen vollständigen, eigenständigen Prompt zum Einfügen in eine separate Claude-Sitzung —
+ /// die Lehrkraft kann sich das eigentliche Material (Tafelbild, Arbeitsblatt, ...) dort selbst
+ /// erzeugen lassen, ohne dass das über das eigene (kostenpflichtige) KI-Backend laufen muss.
+ /// Nur der kurze Vorschlagstext kommt von der KI, der restliche Kontext wird rein lokal aus
+ /// bereits vorhandenen Daten zusammengesetzt. Rein, ohne I/O.
+ ///
+ public string BuildMaterialPrompt(Unit unit, AiLesson lesson, AiPhaseStep phase)
+ {
+ var group = groups.GetById(unit.GroupId);
+ var subject = group?.SubjectId is { } subjectId ? subjects.GetById(subjectId) : null;
+ var dateText = lesson.Date?.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture) ?? "noch offen";
+
+ var lines = new List
+ {
+ $"Ich unterrichte {subject?.Name ?? "unbekanntes Fach"}, Klassenstufe {group?.GradeLevel ?? 0}, Gruppe {group?.Name ?? "-"}.",
+ $"Einheit: „{unit.Title}“",
+ $"Stunde: „{lesson.Topic}“ (Datum: {dateText})",
+ $"Phase: „{phase.Name}“, {phase.DurationMinutes} Min., Tätigkeit: {phase.Activity}",
+ };
+ if (!string.IsNullOrWhiteSpace(phase.Material))
+ lines.Add($"Bisheriges Material laut Planung: {phase.Material}");
+ lines.Add("");
+ lines.Add("Empfehlung aus der Unterrichtsplanung, was das Material zeigen/enthalten sollte:");
+ lines.Add(phase.MaterialSuggestion ?? "");
+ lines.Add("");
+ lines.Add("Bitte erstelle mir auf dieser Grundlage ein passendes Material für diese Phase " +
+ "(z.B. Tafelbild, Arbeitsblatt oder Visualisierung).");
+
+ return string.Join("\n", lines);
+ }
+
///
/// Ersetzt in (dem tatsächlichen Datenbankstand) jede Lesson, deren Id
/// auch in vorkommt, durch die Entwurfsversion, und hängt Entwürfe ohne
diff --git a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
index 62376df..4b445b7 100644
--- a/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
+++ b/LehrerApp.Desktop/ViewModels/Groups/PlanningViewModels.cs
@@ -1085,6 +1085,11 @@ public partial class GenerateLessonSeriesDialogViewModel : ObservableObject
// ── Dialog: KI-gestützte Planungsunterstützung (4.5.9) ───────────────────────
+/// Ein kopierbarer Prompt für ein von der KI vorgeschlagenes Material/Medium zu einer Phase (siehe
+/// AiPlanningService.BuildMaterialPrompt) — gedacht zum Einfügen in eine separate Claude-Sitzung,
+/// nicht über das eigene KI-Backend erzeugt.
+public record MaterialPromptItem(string PhaseName, string SuggestionText, string PromptText);
+
/// Eine von der KI vorgeschlagene Stunde in der Prüfliste des Dialogs — angehakt = wird beim
/// "Übernehmen" mit übertragen. unterscheidet neu/geändert (siehe
/// AiPlanningDtos.cs), steuert hier nur die Anzeige ("Neu"/"Geändert").
@@ -1098,15 +1103,21 @@ public partial class AiLessonReviewItem : ObservableObject
/// Aufzählung fürs UI — leer bei neuen Vorschlägen, da es dort nichts zu vergleichen gibt.
public string DiffText { get; }
+ /// Medienvorschläge je Phase dieser Lesson (nur Phasen mit gesetztem
+ /// AiPhaseStep.MaterialSuggestion) — leer, wenn die KI für keine Phase einen Vorschlag hatte.
+ public IReadOnlyList MaterialPrompts { get; }
+
[ObservableProperty] private bool _accepted = true;
- public AiLessonReviewItem(AiLesson source, bool isNew, List? fieldDiffs = null)
+ public AiLessonReviewItem(AiLesson source, bool isNew, List? fieldDiffs = null,
+ List? materialPrompts = null)
{
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})";
DiffText = fieldDiffs is { Count: > 0 } ? string.Join("\n", fieldDiffs.Select(d => "• " + d)) : "";
+ MaterialPrompts = materialPrompts ?? [];
}
}
@@ -1168,7 +1179,11 @@ public partial class AiAssistDialogViewModel : ObservableObject
// dem Nutzer eine Auswahl zu zeigen, die ApplyResponse ohnehin verwerfen würde.
if (isExisting && !AllowModifyingExisting) continue;
var fieldDiffs = isExisting ? _aiPlanning.DescribeChanges(existingLessons[l.Id!.Value], l) : null;
- ReviewItems.Add(new AiLessonReviewItem(l, isNew: !isExisting, fieldDiffs));
+ var materialPrompts = l.Phases
+ .Where(p => !string.IsNullOrWhiteSpace(p.MaterialSuggestion))
+ .Select(p => new MaterialPromptItem(p.Name, p.MaterialSuggestion!, _aiPlanning.BuildMaterialPrompt(_unit, l, p)))
+ .ToList();
+ ReviewItems.Add(new AiLessonReviewItem(l, isNew: !isExisting, fieldDiffs, materialPrompts));
}
Summary = response.Summary;
HasResults = true;
diff --git a/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml b/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml
index ed243c4..8c0f2da 100644
--- a/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml
+++ b/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml
@@ -25,6 +25,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml.cs b/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml.cs
index 513515a..906a43f 100644
--- a/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml.cs
+++ b/LehrerApp.Desktop/Views/Groups/AiAssistDialog.axaml.cs
@@ -1,6 +1,9 @@
using Avalonia.Controls;
+using Avalonia.Input.Platform;
using Avalonia.Interactivity;
+using LehrerApp.Desktop.Services;
using LehrerApp.Desktop.ViewModels.Groups;
+using Microsoft.Extensions.DependencyInjection;
namespace LehrerApp.Desktop.Views.Groups;
@@ -8,6 +11,16 @@ public partial class AiAssistDialog : Window
{
public AiAssistDialog() => InitializeComponent();
+ private async void OnCopyMaterialPrompt(object? s, RoutedEventArgs e)
+ {
+ if (s is not Button { DataContext: MaterialPromptItem item }) return;
+ var clipboard = TopLevel.GetTopLevel(this)?.Clipboard;
+ if (clipboard is null) return;
+
+ await clipboard.SetTextAsync(item.PromptText);
+ App.Services.GetRequiredService().ShowSuccess("Prompt in die Zwischenablage kopiert.");
+ }
+
private async void OnSend(object? s, RoutedEventArgs e)
{
if (DataContext is AiAssistDialogViewModel vm && vm.SendCommand.CanExecute(null))
diff --git a/TODO.md b/TODO.md
index f819c28..10bec20 100644
--- a/TODO.md
+++ b/TODO.md
@@ -806,6 +806,32 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
durch eine übernommene KI-Antwort überschreiben, falls sich die Einheit seit dem Absenden
der Anfrage schon geändert hat (aktuell nicht erkannt/abgesichert). Größerer Umbau als
4.5.14 (Teil-Merge-UI je Phase, Änderungszeitpunkt-Vergleich) — deshalb zurückgestellt.
+- [x] **4.5.20** Material-/Medienvorschlag mit Zwischenablage-Prompt für externe Erzeugung
+ (Nutzer-Feedback nach erstem echten Einsatz: eine KI-Stunde ist "nicht die eigene Idee",
+ Material/Medien wie Tafelbilder sollen die KI zumindest vorschlagen können, ohne dass die
+ eigentliche Erzeugung über das eigene, kostenpflichtige Backend laufen muss).
+ **Umsetzung:** `AiPhaseStep` bekommt ein neues optionales Feld `MaterialSuggestion` (kurzer,
+ konkreter Vorschlag, was ein Medium zu dieser Phase zeigen/enthalten sollte, z.B. Aufbau
+ eines Tafelbilds) — der Systemprompt in `plan.php` weist die KI an, das nur zu füllen, wenn
+ ein Medium über das ohnehin genannte `material`/`shorthand` hinaus wirklich einen Mehrwert
+ hätte. Bewusst **kein** neues Feld auf dem Domänenmodell `LessonPhaseStep`: der Vorschlag ist
+ nur für die Review-Anzeige/Prompt-Erzeugung relevant, keine Migration nötig, geht beim
+ "Übernehmen" bewusst nicht in die gespeicherte Lesson ein (siehe 4.5.21 für dauerhaftes
+ Speichern als mögliche spätere Erweiterung). `AiPlanningService.BuildMaterialPrompt` baut
+ daraus rein lokal (kein weiterer KI-Aufruf) einen vollständigen, eigenständigen Prompt aus
+ Fach/Stufe/Gruppe, Einheit, Stunde und Phase plus dem KI-Vorschlag; im `AiAssistDialog`
+ erscheint dieser als Kasten unter der jeweiligen Stunde mit Button "📋 Prompt kopieren
+ (extern erzeugen)" (Avalonia-`IClipboard`, erste Zwischenablage-Nutzung in der App), den die
+ Lehrkraft in eine eigene Claude-Sitzung einfügt. Kein neuer Server-Roundtrip: nur ein kurzer
+ zusätzlicher Textabschnitt je Phase in der ohnehin laufenden Planungsantwort.
+- [ ] **4.5.21** (Zurückgestellt, Nutzer-Idee neben 4.5.20) "Schattenfeld" mit didaktischem
+ Hintergrund/Begründung je KI-Stunde bzw. -Phase, einklappbar/auf Nachfrage sichtbar — bei
+ einer KI-generierten Stunde fehlt anders als bei einer selbst geschriebenen das eigene
+ Vorwissen zum "Warum", das für einen sicheren Unterrichtseinsatz hilft. Zwei Bauweisen
+ abzuwägen: immer mitgeneriert (höhere Kosten bei jeder Anfrage, auch wenn ungenutzt) vs. nur
+ auf Klick nachgeladen (wiederverwendet den Nachfassen-Mechanismus aus 4.5.17, kostet nur bei
+ tatsächlicher Nutzung) — Letzteres passt besser zum bestehenden Guthabenmodell. Noch nicht
+ entschieden/umgesetzt.
---
diff --git a/ai-backend/plan.php b/ai-backend/plan.php
index 13057fe..9f3ce37 100644
--- a/ai-backend/plan.php
+++ b/ai-backend/plan.php
@@ -106,6 +106,13 @@ Das Feld "allowModifyingExistingLessons" in der Eingabe legt fest, was du vorsch
"AB" Arbeitsblatt) — kein Fließtext.
- "durationMinutes" je Phase sollte in Summe zur für die Stunde realistischen Zeit passen
(i.d.R. rund 45 Minuten je Einzelstunde, abzüglich organisatorischer Zeit).
+- "materialSuggestion" je Phase (nur in deiner Antwort, nicht in der Eingabe): ein kurzer Vorschlag
+ (1-2 Sätze), WAS ein zu dieser Phase passendes Medium/Material konkret zeigen oder enthalten
+ sollte (z.B. Aufbau eines Tafelbilds, Inhalt eines Arbeitsblatts) — nicht nur "ein Tafelbild
+ wäre hilfreich", sondern was darauf stehen sollte. Nur setzen, wenn ein Medium über das bereits
+ in "material"/"shorthand" Genannte hinaus wirklich einen Mehrwert hätte, sonst weglassen (null).
+ Die Lehrkraft erzeugt das Medium separat selbst mit diesem Vorschlag als Grundlage — plane hier
+ keine Umsetzungsdetails wie Layout oder Werkzeug.
## Antwortformat
@@ -125,7 +132,8 @@ Antworte AUSSCHLIESSLICH mit gültigem JSON (kein Freitext davor/danach) in gena
"activity": "",
"material": "",
"shorthand": "",
- "alternativePathName": ""
+ "alternativePathName": "",
+ "materialSuggestion": ""
}
],
"homework": "",