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:
2026-08-16 01:31:14 +02:00
parent 2b4fda7bb3
commit 8495e1b8d0
40 changed files with 2326 additions and 51 deletions
@@ -0,0 +1,52 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Groups"
x:Class="LehrerApp.Desktop.Views.Groups.AiAssistDialog"
x:DataType="vm:AiAssistDialogViewModel"
Title="KI-Unterstützung"
Width="480" Height="560" MinWidth="420" MinHeight="420"
CanResize="True" WindowStartupLocation="CenterOwner">
<Grid RowDefinitions="*,Auto" Margin="24">
<ScrollViewer Grid.Row="0">
<StackPanel Spacing="14" Margin="0,0,12,0">
<TextBlock Text="KI-Unterstützung" Classes="dialogtitle"/>
<TextBlock Text="{Binding UnitSummary}" FontSize="12" Opacity="0.6"/>
<StackPanel Spacing="4" IsVisible="{Binding !HasResults}">
<TextBlock Text="Anweisung" FontSize="12" Opacity="0.7"/>
<TextBox Text="{Binding Instruction}" AcceptsReturn="True" TextWrapping="Wrap" Height="120"
PlaceholderText="z.B. Ergänze zwei weitere Stunden zum Thema Redoxreaktionen mit steigendem Anspruch."
IsEnabled="{Binding !IsBusy}"/>
</StackPanel>
<TextBlock Text="Anfrage läuft…" FontSize="12" Opacity="0.6" IsVisible="{Binding IsBusy}"/>
<TextBlock Text="{Binding ErrorMessage}" Foreground="Red" FontSize="12" TextWrapping="Wrap"
IsVisible="{Binding ErrorMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<StackPanel Spacing="10" IsVisible="{Binding HasResults}">
<TextBlock Text="{Binding Summary}" FontSize="12" TextWrapping="Wrap"
IsVisible="{Binding Summary, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Text="Vorschläge (angehakt wird übernommen)" FontSize="12" Opacity="0.7"/>
<ItemsControl ItemsSource="{Binding ReviewItems}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:AiLessonReviewItem">
<CheckBox Content="{Binding DisplayLabel}" IsChecked="{Binding Accepted}" Margin="0,3"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBlock Text="Die KI hat keine Stunden vorgeschlagen." Opacity="0.6" FontSize="12"
IsVisible="{Binding !ReviewItems.Count}"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,8,*" Margin="0,20,0,0">
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
<Button Grid.Column="2" Content="Anfrage senden" HorizontalAlignment="Stretch" Click="OnSend"
IsVisible="{Binding !HasResults}" IsEnabled="{Binding !IsBusy}"/>
<Button Grid.Column="2" Content="Übernehmen" HorizontalAlignment="Stretch" Click="OnApply"
IsVisible="{Binding HasResults}"/>
</Grid>
</Grid>
</Window>
@@ -0,0 +1,31 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using LehrerApp.Desktop.ViewModels.Groups;
namespace LehrerApp.Desktop.Views.Groups;
public partial class AiAssistDialog : Window
{
public AiAssistDialog() => InitializeComponent();
private async void OnSend(object? s, RoutedEventArgs e)
{
if (DataContext is AiAssistDialogViewModel vm && vm.SendCommand.CanExecute(null))
await vm.SendCommand.ExecuteAsync(null);
}
private void OnApply(object? s, RoutedEventArgs e)
{
if (DataContext is AiAssistDialogViewModel vm && vm.ApplyCommand.CanExecute(null))
{
vm.ApplyCommand.Execute(null);
Close(true);
}
}
private void OnCancel(object? s, RoutedEventArgs e)
{
if (DataContext is AiAssistDialogViewModel vm) vm.CancelCommand.Execute(null);
Close(false);
}
}
@@ -15,6 +15,7 @@
<Button Content="Bearbeiten" Command="{Binding EditUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="Als Vorlage kopieren" Command="{Binding CopyUnitCommand}"/>
<Button Content="Löschen" Command="{Binding DeleteUnitCommand}" IsEnabled="{Binding !IsReadOnly}"/>
<Button Content="🤖 KI-Unterstützung" Command="{Binding AiAssistCommand}"/>
</StackPanel>
</Grid>
@@ -30,6 +30,7 @@ public partial class PlanningTabView : UserControl
vm.OnPickMoveTarget = ShowMoveLessonDialog;
vm.OnShowLesson = ShowLessonViewerDialog;
vm.OnGenerateLessonSeries = ShowGenerateLessonSeriesDialog;
vm.OnAiAssist = ShowAiAssistDialog;
}
}
@@ -147,4 +148,20 @@ public partial class PlanningTabView : UserControl
App.Services.GetRequiredService<NotificationService>().ShowSuccess(result.Summary);
return ok ? dialogVm.Result : null;
}
private async Task<bool> ShowAiAssistDialog(Unit unit)
{
var dialogVm = new AiAssistDialogViewModel(
App.Services.GetRequiredService<AiPlanningService>(),
App.Services.GetRequiredService<AiSettingsService>(),
App.Services.GetRequiredService<ILessonRepository>(),
unit);
var dialog = new AiAssistDialog { DataContext = dialogVm };
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner is null) return false;
await dialog.ShowDialog<bool>(owner);
return dialogVm.Result;
}
}