KI-Feature 4.5.20: Medienvorschlag + Zwischenablage-Prompt für externe Erzeugung
Die KI liefert je Verlaufsplan-Phase optional einen kurzen Vorschlag, was ein Medium/Material (z.B. Tafelbild) zeigen sollte (AiPhaseStep.MaterialSuggestion), nur wenn das über das ohnehin genannte Material hinaus einen Mehrwert hätte. Bewusst nicht ins Domänenmodell übernommen (keine Migration nötig) — nur für die Review-Anzeige relevant. AiPlanningService.BuildMaterialPrompt baut daraus rein lokal (kein weiterer KI-Aufruf) einen vollständigen Prompt aus Fach/Stufe/Gruppe, Einheit, Stunde und Phase. Im AiAssistDialog landet er per Klick in der Zwischenablage, zum Einfügen in eine separate Claude-Sitzung für die eigentliche Materialerzeugung. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -249,6 +249,39 @@ public class AiPlanningService(HttpClient http, ILessonRepository lessons,
|
||||
return diffs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Baut aus Einheit/Stunde/Phase und dem KI-Medienvorschlag (<see cref="AiPhaseStep.MaterialSuggestion"/>)
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string>
|
||||
{
|
||||
$"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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ersetzt in <paramref name="saved"/> (dem tatsächlichen Datenbankstand) jede Lesson, deren Id
|
||||
/// auch in <paramref name="draft"/> vorkommt, durch die Entwurfsversion, und hängt Entwürfe ohne
|
||||
|
||||
@@ -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. <see cref="AiLesson.Id"/> unterscheidet neu/geändert (siehe
|
||||
/// AiPlanningDtos.cs), <see cref="IsNew"/> 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<MaterialPromptItem> MaterialPrompts { get; }
|
||||
|
||||
[ObservableProperty] private bool _accepted = true;
|
||||
|
||||
public AiLessonReviewItem(AiLesson source, bool isNew, List<string>? fieldDiffs = null)
|
||||
public AiLessonReviewItem(AiLesson source, bool isNew, List<string>? fieldDiffs = null,
|
||||
List<MaterialPromptItem>? 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;
|
||||
|
||||
@@ -25,6 +25,19 @@
|
||||
<TextBlock Text="{Binding DiffText}" FontSize="11" Opacity="0.6" TextWrapping="Wrap"
|
||||
Margin="24,0,0,0"
|
||||
IsVisible="{Binding DiffText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<ItemsControl ItemsSource="{Binding MaterialPrompts}" Margin="24,2,0,0">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:MaterialPromptItem">
|
||||
<Border Background="#14000000" CornerRadius="4" Padding="6" Margin="0,2">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding SuggestionText}" FontSize="11" TextWrapping="Wrap"/>
|
||||
<Button Content="📋 Prompt kopieren (extern erzeugen)" FontSize="11"
|
||||
Click="OnCopyMaterialPrompt"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
|
||||
@@ -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<NotificationService>().ShowSuccess("Prompt in die Zwischenablage kopiert.");
|
||||
}
|
||||
|
||||
private async void OnSend(object? s, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is AiAssistDialogViewModel vm && vm.SendCommand.CanExecute(null))
|
||||
|
||||
Reference in New Issue
Block a user