Files
LehrerApp/LehrerApp.Desktop/ViewModels/Settings/SettingsViewModel.AiSettings.cs
T
2026-10-22 11:40:19 +02:00

170 lines
7.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LehrerApp.Desktop.Services;
using System.Collections.ObjectModel;
using System.Globalization;
namespace LehrerApp.Desktop.ViewModels.Settings;
public partial class SettingsViewModel
{
[ObservableProperty] private bool _aiEnabled;
[ObservableProperty] private string _aiUsername = "";
[ObservableProperty] private string _aiPassword = "";
[ObservableProperty] private string _aiLoginError = "";
[ObservableProperty] private bool _aiIsLoggedIn;
[ObservableProperty] private string _aiBalanceDisplay = "";
[ObservableProperty] private bool _aiIsRefreshing;
[ObservableProperty] private string _aiAccountStatus = "";
[ObservableProperty] private string _aiUsageSummary = "";
[ObservableProperty] private string _aiUsageStatus = "Nach der Anmeldung erscheint hier der Verbrauch der letzten vier Wochen.";
[ObservableProperty] private string _aiUsagePeriod = "";
[ObservableProperty] private bool _aiHasUsage;
private int _aiRefreshVersion;
public ObservableCollection<AiFeatureItem> AiFeatures { get; } =
[
new("plan", "Unterricht planen", "Einheiten und einzelne Stunden entwerfen oder überarbeiten; Vorschläge vor der Übernahme prüfen."),
new("explain", "Didaktik erklären", "Didaktische Erläuterungen zu einer vorgeschlagenen Unterrichtsstunde abrufen."),
new("gbu", "Gefährdungsbeurteilung", "Einen GBU-Entwurf zum geplanten Experiment erstellen und im Assistenten weiterbearbeiten."),
new("untis-status", "Fehlzeiten prüfen", "Vorschläge für den Entschuldigungsstatus beim WebUntis-Abgleich erhalten."),
new(null, "Stoffdaten nachschlagen", "Stoffe im GBU-Assistenten in der RiSU-Stoffliste suchen. Keine KI, kein Guthabenverbrauch.", "Kostenlos")
];
public ObservableCollection<AiUsageGroupItem> AiUsageGroups { get; } = [];
public ObservableCollection<AiUsageEntryItem> AiUsageEntries { get; } = [];
private void LoadAiSettings()
{
AiEnabled = _aiSettings.Enabled;
AiUsername = _aiSettings.Username;
AiIsLoggedIn = _aiSettings.IsLoggedIn;
if (AiIsLoggedIn) _ = RefreshAiAccount();
}
partial void OnAiEnabledChanged(bool value) => _aiSettings.SetEnabled(value);
[RelayCommand]
private async Task RefreshAiAccount()
{
var token = _aiSettings.GetToken();
if (token is null) return;
var version = ++_aiRefreshVersion;
bool Current() => version == _aiRefreshVersion && token == _aiSettings.GetToken();
AiIsRefreshing = true;
AiAccountStatus = "";
AiBalanceDisplay = "Guthaben wird geladen …";
ClearAiUsage("Verbrauch wird geladen …");
try
{
// Die Übersicht bleibt nutzbar, wenn ein älteres Backend nur das Guthaben liefert.
try
{
var balance = await _aiPlanning.GetAccountBalanceAsync(token);
if (!Current()) return;
AiBalanceDisplay = "Guthaben: " + AiMoney.Format(balance.BalanceUsd, balance.BalanceEur);
}
catch (AiBackendException ex)
{
if (!Current()) return;
AiBalanceDisplay = "Guthaben nicht verfügbar";
AiAccountStatus = ex.Message;
}
try
{
var usage = await _aiPlanning.GetUsageAsync(token);
if (!Current()) return;
AiUsageStatus = usage.Calls == 0 ? $"In den letzten {usage.Days} Tagen wurde nichts berechnet." : "";
AiUsagePeriod = $"{usage.From:dd.MM.yyyy HH:mm} bis {usage.To:dd.MM.yyyy HH:mm} (Backend-Zeit)";
AiUsageSummary = $"Gesamt: {AiMoney.Format(usage.ChargedUsd, usage.ChargedEur)} · {usage.Calls} Aufrufe";
foreach (var group in usage.ByAction.OrderByDescending(g => g.ChargedUsd))
AiUsageGroups.Add(new(group.Label, group.Calls, AiMoney.Format(group.ChargedUsd, group.ChargedEur),
usage.ChargedUsd > 0 ? (double)(100 * group.ChargedUsd / usage.ChargedUsd) : 0));
foreach (var entry in usage.Entries.OrderByDescending(e => e.At))
AiUsageEntries.Add(new(entry.At.ToString("dd.MM.yyyy HH:mm"), entry.Label,
AiMoney.Format(entry.ChargedUsd, entry.ChargedEur)));
foreach (var feature in AiFeatures.Where(f => f.ActionId is not null))
{
var groups = usage.ByAction.Where(g => g.ActionId == feature.ActionId).ToArray();
feature.Usage = groups.Length == 0 ? "Keine berechneten Aufrufe" :
$"{groups.Sum(g => g.Calls)}× · {AiMoney.Format(groups.Sum(g => g.ChargedUsd), groups.All(g => g.ChargedEur.HasValue) ? groups.Sum(g => g.ChargedEur!.Value) : null)}";
}
AiHasUsage = usage.Calls > 0;
if (usage.EntriesTruncated)
AiUsageStatus = $"Die Liste zeigt die neuesten {usage.Entries.Count} Aufrufe. Die Summen enthalten alle Aufrufe im Zeitraum.";
}
catch (AiBackendException ex)
{
if (Current()) AiUsageStatus = ex.Message;
}
}
finally { if (Current()) AiIsRefreshing = false; }
}
private void ClearAiUsage(string status)
{
AiUsageGroups.Clear();
AiUsageEntries.Clear();
AiHasUsage = false;
AiUsageSummary = "";
AiUsagePeriod = "";
AiUsageStatus = status;
foreach (var feature in AiFeatures.Where(f => f.ActionId is not null)) feature.Usage = "Verbrauch nicht verfügbar";
}
[RelayCommand]
private async Task AiLogin()
{
AiLoginError = "";
var valid = true;
if (string.IsNullOrWhiteSpace(AiUsername)) { AiLoginError = "Benutzername erforderlich."; valid = false; }
if (string.IsNullOrWhiteSpace(AiPassword)) { AiLoginError = "Passwort erforderlich."; valid = false; }
if (!valid) return;
try
{
var token = await _aiPlanning.LoginAsync(AiUsername, AiPassword);
_aiSettings.SetCredentialsAndToken(AiUsername, token);
AiIsLoggedIn = true;
await RefreshAiAccount();
}
catch (AiBackendException ex) { AiLoginError = ex.Message; }
finally { AiPassword = ""; }
}
[RelayCommand]
private void AiLogout()
{
++_aiRefreshVersion;
_aiSettings.Logout();
AiIsLoggedIn = false;
AiIsRefreshing = false;
AiBalanceDisplay = "";
AiAccountStatus = "";
AiLoginError = "";
AiPassword = "";
ClearAiUsage("Nach der Anmeldung erscheint hier der Verbrauch der letzten vier Wochen.");
}
}
public partial class AiFeatureItem(string? actionId, string name, string description, string usage = "Verbrauch nach Anmeldung") : ObservableObject
{
public string? ActionId { get; } = actionId;
public string Name { get; } = name;
public string Description { get; } = description;
[ObservableProperty] private string _usage = usage;
}
public sealed record AiUsageGroupItem(string Label, int Calls, string Amount, double Percent)
{
public string CallsDisplay => $"{Calls}×";
}
public sealed record AiUsageEntryItem(string When, string Label, string Amount);
internal static class AiMoney
{
public static string Format(decimal usd, decimal? eur) => Number(usd) + " USD" +
(eur is { } value ? $" (≈ {Number(value)} €)" : "");
private static string Number(decimal value) => value.ToString(value != 0 && Math.Abs(value) < 0.1m ? "0.000000" : "0.00", CultureInfo.GetCultureInfo("de-DE"));
}