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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="clr-namespace:LehrerApp.Desktop.ViewModels.Settings"
|
||||
x:Class="LehrerApp.Desktop.Views.Settings.CompetencyCatalogImportDialog"
|
||||
x:DataType="vm:CompetencyCatalogImportViewModel"
|
||||
Title="Kompetenzkatalog importieren"
|
||||
Width="760" Height="720" MinWidth="620" MinHeight="560"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,*,Auto" Margin="24,20">
|
||||
<StackPanel Grid.Row="0" Spacing="10">
|
||||
<TextBlock Text="Import prüfen" Classes="dialogtitle"/>
|
||||
<TextBlock Text="{Binding Target}" FontWeight="SemiBold"/>
|
||||
<TextBlock Text="{Binding Summary}" FontSize="12" Opacity="0.7"/>
|
||||
|
||||
<StackPanel IsVisible="{Binding HasWarnings}" Spacing="4">
|
||||
<TextBlock Text="Hinweise zur Datei" FontWeight="SemiBold" Foreground="#B06A00"/>
|
||||
<ItemsControl ItemsSource="{Binding Warnings}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding}" TextWrapping="Wrap" FontSize="12" Foreground="#B06A00"/>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Importverfahren" FontSize="12" Opacity="0.7"/>
|
||||
<ComboBox ItemsSource="{Binding Modes}" SelectedItem="{Binding SelectedMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ImportModeOption">
|
||||
<TextBlock Text="{Binding Label}"/>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<Border IsVisible="{Binding IsMerge}" Background="#143B82F6" CornerRadius="5" Padding="10,8">
|
||||
<TextBlock Text="Neue Einträge werden ergänzt. Bei Konflikten bleibt ohne abweichende Auswahl die vorhandene Fassung erhalten."
|
||||
TextWrapping="Wrap" FontSize="12"/>
|
||||
</Border>
|
||||
<Border IsVisible="{Binding IsReplace}" Background="#20C62828" CornerRadius="5" Padding="10,8">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Achtung: Alle vorhandenen Bereiche und Kompetenzen dieses Fachs und dieser Klassenstufe werden durch den Dateiinhalt ersetzt."
|
||||
TextWrapping="Wrap" Foreground="#C62828" FontWeight="SemiBold"/>
|
||||
<CheckBox Content="Ich möchte den vorhandenen Katalog vollständig ersetzen."
|
||||
IsChecked="{Binding ReplaceConfirmed}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1" RowDefinitions="Auto,*" Margin="0,14,0,0" IsVisible="{Binding HasConflicts}">
|
||||
<TextBlock Grid.Row="0" Text="Konflikte" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,6"/>
|
||||
<ScrollViewer Grid.Row="1">
|
||||
<ItemsControl ItemsSource="{Binding Conflicts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:CompetencyImportConflictItem">
|
||||
<Border BorderBrush="{DynamicResource SystemControlForegroundBaseLowBrush}"
|
||||
BorderThickness="1" CornerRadius="5" Padding="12" Margin="0,0,0,8">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="{Binding Location}" FontWeight="SemiBold"/>
|
||||
<Grid ColumnDefinitions="110,*" RowDefinitions="Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Vorhanden:" FontSize="12" Opacity="0.65"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ExistingValue}" TextWrapping="Wrap" FontSize="12"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Importiert:" FontSize="12" Opacity="0.65"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ImportedValue}" TextWrapping="Wrap" FontSize="12"/>
|
||||
</Grid>
|
||||
<CheckBox Content="Importierte Fassung übernehmen"
|
||||
IsChecked="{Binding UseImported}"
|
||||
IsVisible="{Binding $parent[Window].((vm:CompetencyCatalogImportViewModel)DataContext).IsMerge}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Grid.Row="2" Spacing="10" Margin="0,14,0,0">
|
||||
<TextBlock Text="{Binding Error}" Foreground="Red" TextWrapping="Wrap"
|
||||
IsVisible="{Binding Error, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Grid ColumnDefinitions="*,10,*">
|
||||
<Button Grid.Column="0" Content="Abbrechen" HorizontalAlignment="Stretch" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="Import anwenden" HorizontalAlignment="Stretch" Click="OnApply"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using LehrerApp.Desktop.ViewModels.Settings;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Settings;
|
||||
|
||||
public partial class CompetencyCatalogImportDialog : Window
|
||||
{
|
||||
public CompetencyCatalogImportDialog() => InitializeComponent();
|
||||
|
||||
private void OnApply(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is CompetencyCatalogImportViewModel vm && vm.TryApply())
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -711,6 +711,43 @@
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
<!-- Tab: KI-Unterstützung (4.5.9) -->
|
||||
<ContentPage Header="KI-Unterstützung">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="32,20,32,28" Spacing="14" MaxWidth="420">
|
||||
|
||||
<TextBlock Text="KI-gestützte Planungsunterstützung" FontSize="16" FontWeight="SemiBold"/>
|
||||
<TextBlock FontSize="12" Opacity="0.6" TextWrapping="Wrap"
|
||||
Text="Ermöglicht KI-Vorschläge für Unterrichtseinheiten über einen Zwischendienst auf dem eigenen Server (kein API-Schlüssel im Client). Jede Anfrage verbraucht Guthaben."/>
|
||||
|
||||
<CheckBox Content="KI-Unterstützung aktivieren" IsChecked="{Binding AiEnabled}"/>
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding !AiIsLoggedIn}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Benutzername" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding AiUsername}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Passwort" FontSize="12" Opacity="0.7"/>
|
||||
<TextBox Text="{Binding AiPassword}" PasswordChar="●"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding AiLoginError}" Foreground="Red" FontSize="12"
|
||||
IsVisible="{Binding AiLoginError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||
<Button Content="Anmelden" Command="{Binding AiLoginCommand}" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding AiIsLoggedIn}">
|
||||
<TextBlock FontSize="13" FontWeight="SemiBold">
|
||||
<Run Text="Angemeldet als: "/><Run Text="{Binding AiUsername}"/>
|
||||
</TextBlock>
|
||||
<TextBlock Text="{Binding AiBalanceDisplay}" FontSize="13"/>
|
||||
<Button Content="Abmelden" Command="{Binding AiLogoutCommand}" HorizontalAlignment="Left"/>
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</ContentPage>
|
||||
|
||||
</TabbedPage>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -64,7 +64,16 @@ public partial class SettingsView : UserControl
|
||||
|
||||
if (files.Count == 0) return;
|
||||
var json = await File.ReadAllTextAsync(files[0].Path.LocalPath);
|
||||
vm.ImportCatalog(json);
|
||||
var preview = vm.PrepareCatalogImport(json);
|
||||
if (preview is null) return;
|
||||
|
||||
var owner = topLevel as Window;
|
||||
if (owner is null) return;
|
||||
var dialog = new CompetencyCatalogImportDialog
|
||||
{
|
||||
DataContext = new CompetencyCatalogImportViewModel(vm, preview),
|
||||
};
|
||||
await dialog.ShowDialog<bool>(owner);
|
||||
}
|
||||
|
||||
private async void OnExportClick(object? sender, RoutedEventArgs e)
|
||||
|
||||
Reference in New Issue
Block a user