170 lines
7.6 KiB
C#
170 lines
7.6 KiB
C#
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"));
|
||
}
|