feat: add AI administration tab with provider dialog, test and usage overview
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1da4423c52
commit
65993c268b
5 files changed
+426
No files matched your search
@@ -0,0 +1,175 @@
|
||||
@using FamilyPlanner.Domain.Ai
|
||||
@using FamilyPlanner.Shared.Ai
|
||||
@inject IAiApi Api
|
||||
@inject IDialogService DialogService
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<div class="d-flex align-center flex-wrap gap-2 mb-2">
|
||||
<MudText Typo="Typo.h6" HtmlTag="h2">KI-Anbieter</MudText>
|
||||
<MudSpacer />
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="() => EditAsync(null)" Class="fp-add-ai">Anbieter hinzufügen</MudButton>
|
||||
</div>
|
||||
<MudText Typo="Typo.body2" Class="mb-3">
|
||||
Die KI erzeugt Aufgaben und prüft Lösungen. Cloud-Anbieter erhalten nur Texte; Fotos gehen nur an Anbieter im Heimnetz oder mit ausdrücklicher Freigabe.
|
||||
Nur Eltern können KI-Funktionen auslösen.
|
||||
</MudText>
|
||||
|
||||
@if (_providers is null)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else if (_providers.Count == 0)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="fp-ai-empty">Noch kein Anbieter eingerichtet. Ohne Anbieter sind alle KI-Funktionen ausgeblendet.</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable T="AiProviderDto" Items="_providers" Breakpoint="Breakpoint.Sm" Elevation="0" Outlined="true" Class="fp-ai-table">
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Modell</MudTh>
|
||||
<MudTh>Einsatz</MudTh>
|
||||
<MudTh>Datenschutz</MudTh>
|
||||
<MudTh><span class="sr-only">Aktionen</span></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Name">
|
||||
<span class="fp-ai-name">@context.Name</span>
|
||||
<MudText Typo="Typo.caption">@AiRules.Info(context.Type).DisplayName · @context.BaseUrl</MudText>
|
||||
@if (!context.IsActive)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Class="fp-ai-inactive">inaktiv</MudChip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Modell">@context.Model</MudTd>
|
||||
<MudTd DataLabel="Einsatz">@DescribePurposes(context.Purposes)</MudTd>
|
||||
<MudTd DataLabel="Datenschutz">
|
||||
@if (context.IsHomeNetwork)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Outlined" Class="fp-ai-home">Heimnetz</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Variant="Variant.Outlined" Class="fp-ai-cloud">Cloud</MudChip>
|
||||
}
|
||||
<MudText Typo="Typo.caption">
|
||||
Bilder @(context.IsHomeNetwork || context.AllowImages ? "erlaubt" : "gesperrt") · @(context.HasApiKey ? "Schlüssel gesetzt" : "kein Schlüssel") ·
|
||||
@(context.DailyLimitPerUser == 0 ? "ohne Tageslimit" : $"{context.DailyLimitPerUser}/Tag")
|
||||
</MudText>
|
||||
</MudTd>
|
||||
<MudTd Style="text-align:right">
|
||||
<MudButton Size="Size.Small" Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.NetworkCheck" Disabled="@(_testing == context.Id)"
|
||||
OnClick="() => TestAsync(context)" Class="fp-ai-test">Testen</MudButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" OnClick="() => EditAsync(context)" aria-label="@($"{context.Name} bearbeiten")" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="() => DeleteAsync(context)" aria-label="@($"{context.Name} löschen")" />
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
|
||||
@if (_testResult is { } test)
|
||||
{
|
||||
<MudAlert Severity="@(test.Result.Success ? Severity.Success : Severity.Error)" Class="mt-3 fp-ai-testresult" role="status">
|
||||
<strong>@test.Name:</strong> @test.Result.Message
|
||||
@if (test.Result.Success)
|
||||
{
|
||||
<span> (@test.Result.DurationMs ms, Antwort: „@test.Result.Reply“)</span>
|
||||
}
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudText Typo="Typo.h6" HtmlTag="h2" Class="mt-6 mb-1">Nutzung</MudText>
|
||||
@if (_usage is null)
|
||||
{
|
||||
<LoadingIndicator />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mb-2 fp-ai-today">Heute: @_usage.CallsToday Aufrufe@(_usage.FailedToday > 0 ? $", davon {_usage.FailedToday} fehlgeschlagen" : string.Empty).</MudText>
|
||||
@if (_usage.Recent.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="fp-ai-nousage">Noch keine KI-Aufrufe.</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTable T="AiUsageDto" Items="_usage.Recent" Breakpoint="Breakpoint.Sm" Elevation="0" Outlined="true" Dense="true" Class="fp-ai-usage">
|
||||
<HeaderContent>
|
||||
<MudTh>Zeit</MudTh>
|
||||
<MudTh>Person</MudTh>
|
||||
<MudTh>Anbieter</MudTh>
|
||||
<MudTh>Zweck</MudTh>
|
||||
<MudTh>Ergebnis</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Zeit">@UserManagement.FormatDate(context.At)</MudTd>
|
||||
<MudTd DataLabel="Person">@context.UserName</MudTd>
|
||||
<MudTd DataLabel="Anbieter">@context.ProviderName (@context.Model)</MudTd>
|
||||
<MudTd DataLabel="Zweck">@AiRules.PurposeName(context.Purpose)@(context.HadImages ? " · mit Bild" : string.Empty)</MudTd>
|
||||
<MudTd DataLabel="Ergebnis">
|
||||
@if (context.Success)
|
||||
{
|
||||
<span>OK · @context.DurationMs ms@(context.InputTokens is { } i ? $" · {i}→{context.OutputTokens} Tokens" : string.Empty)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="fp-ai-failed">Fehler: @context.Error</span>
|
||||
}
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
}
|
||||
}
|
||||
|
||||
@code {
|
||||
private sealed record TestOutcome(string Name, AiTestResultDto Result);
|
||||
|
||||
private List<AiProviderDto>? _providers;
|
||||
private AiUsageOverviewDto? _usage;
|
||||
private TestOutcome? _testResult;
|
||||
private Guid? _testing;
|
||||
|
||||
protected override Task OnInitializedAsync() => LoadAsync();
|
||||
|
||||
private async Task LoadAsync()
|
||||
{
|
||||
_providers = (await Api.GetProvidersAsync()).Value ?? [];
|
||||
_usage = (await Api.GetUsageAsync()).Value;
|
||||
}
|
||||
|
||||
private static string DescribePurposes(AiPurpose purposes) =>
|
||||
purposes == AiPurpose.None ? "–" : string.Join(", ", AiRules.SelectablePurposes.Where(p => purposes.HasFlag(p)).Select(AiRules.PurposeName));
|
||||
|
||||
private async Task EditAsync(AiProviderDto? provider)
|
||||
{
|
||||
var dialog = await DialogService.ShowAsync<AiProviderDialog>(provider is null ? "KI-Anbieter hinzufügen" : $"{provider.Name} bearbeiten",
|
||||
new DialogParameters<AiProviderDialog> { { d => d.Provider, provider } },
|
||||
new DialogOptions { MaxWidth = MaxWidth.Small, FullWidth = true, CloseButton = true, CloseOnEscapeKey = true });
|
||||
if (await dialog.Result is { Canceled: false })
|
||||
{
|
||||
Snackbar.Add("Anbieter gespeichert.", Severity.Success);
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(AiProviderDto provider)
|
||||
{
|
||||
if (await DialogService.ShowMessageBoxAsync("Anbieter löschen?", $"„{provider.Name}“ und der gespeicherte Schlüssel werden entfernt.",
|
||||
yesText: "Löschen", cancelText: "Abbrechen") != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await Api.DeleteProviderAsync(provider.Id);
|
||||
Snackbar.Add(result.Problem?.Message ?? "Anbieter gelöscht.", result.Succeeded ? Severity.Success : Severity.Error);
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private async Task TestAsync(AiProviderDto provider)
|
||||
{
|
||||
_testing = provider.Id;
|
||||
var result = await Api.TestProviderAsync(provider.Id);
|
||||
_testing = null;
|
||||
_testResult = new TestOutcome(provider.Name, result.Value ?? new AiTestResultDto(false, result.Problem?.Message ?? "Der Test ist fehlgeschlagen.", 0, null));
|
||||
_usage = (await Api.GetUsageAsync()).Value ?? _usage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
@using FamilyPlanner.Domain.Ai
|
||||
@using FamilyPlanner.Shared.Ai
|
||||
@inject IAiApi Api
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<EditForm Model="this" OnSubmit="SaveAsync" FormName="ai-provider" id="ai-provider-form">
|
||||
<MudStack Spacing="3">
|
||||
@if (_generalError is not null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" role="alert" Class="fp-dialog-error">@_generalError</MudAlert>
|
||||
}
|
||||
|
||||
<MudTextField @bind-Value="_name" Label="Name" InputId="ai-name" Variant="Variant.Outlined" AutoFocus="true"
|
||||
Placeholder="z. B. Ollama im Keller" Error="@HasError("Name")" ErrorText="@ErrorFor("Name")" />
|
||||
|
||||
<MudSelect T="AiProviderType" Value="_type" ValueChanged="OnTypeChanged" Label="Anbieter" Variant="Variant.Outlined" Class="fp-ai-type"
|
||||
HelperText="@TypeInfo.Hint">
|
||||
@foreach (var info in AiRules.Types)
|
||||
{
|
||||
<MudSelectItem T="AiProviderType" Value="@info.Type">@info.DisplayName</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
<MudTextField @bind-Value="_baseUrl" Label="Adresse" InputId="ai-url" Variant="Variant.Outlined" Placeholder="@TypeInfo.DefaultBaseUrl"
|
||||
HelperText="@UrlHint" Error="@HasError("BaseUrl")" ErrorText="@ErrorFor("BaseUrl")" />
|
||||
|
||||
<MudTextField @bind-Value="_model" Label="Modell" InputId="ai-model" Variant="Variant.Outlined" Placeholder="@TypeInfo.DefaultModel"
|
||||
Error="@HasError("Model")" ErrorText="@ErrorFor("Model")" />
|
||||
|
||||
<MudTextField @bind-Value="_apiKey" Label="@(Provider?.HasApiKey == true ? "API-Schlüssel (leer lassen = unverändert)" : "API-Schlüssel")"
|
||||
InputId="ai-key" Variant="Variant.Outlined" InputType="InputType.Password" Disabled="_clearKey" autocomplete="off"
|
||||
HelperText="Der Schlüssel wird verschlüsselt gespeichert und nie wieder angezeigt."
|
||||
Error="@HasError("ApiKey")" ErrorText="@ErrorFor("ApiKey")" />
|
||||
@if (Provider?.HasApiKey == true)
|
||||
{
|
||||
<MudCheckBox T="bool" @bind-Value="_clearKey" Label="Gespeicherten Schlüssel entfernen" Class="fp-ai-clearkey" />
|
||||
}
|
||||
|
||||
<MudStack Row="true" Spacing="3">
|
||||
<MudNumericField T="int" @bind-Value="_timeout" Label="Zeitlimit (Sekunden)" InputId="ai-timeout" Variant="Variant.Outlined" Min="@AiRules.MinTimeoutSeconds"
|
||||
Max="@AiRules.MaxTimeoutSeconds" Error="@HasError("TimeoutSeconds")" ErrorText="@ErrorFor("TimeoutSeconds")" />
|
||||
<MudNumericField T="int" @bind-Value="_limit" Label="Tageslimit je Person" InputId="ai-limit" Variant="Variant.Outlined" Min="0" Max="@AiRules.MaxDailyLimit"
|
||||
HelperText="0 = unbegrenzt" Error="@HasError("DailyLimitPerUser")" ErrorText="@ErrorFor("DailyLimitPerUser")" />
|
||||
</MudStack>
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="_active" Label="Aktiv" Color="Color.Primary" />
|
||||
|
||||
<MudSwitch T="bool" @bind-Value="_allowImages" Label="Bilder dürfen an diesen Anbieter gesendet werden" Color="Color.Warning" Class="fp-ai-images" />
|
||||
@if (!IsHomeNetwork)
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Dense="true" Class="fp-ai-cloudwarning">
|
||||
Diese Adresse liegt außerhalb des Heimnetzes: Aufgaben und – nur wenn oben erlaubt – Fotos von Heften verlassen das Heimnetz.
|
||||
</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.caption">Heimnetz-Adresse: Bilder bleiben im Heimnetz, die Freigabe ist nicht nötig.</MudText>
|
||||
}
|
||||
|
||||
<div>
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-1">Einsatzzweck</MudText>
|
||||
<MudText Typo="Typo.caption" Class="mb-1">Je Zweck genau ein Anbieter. Ohne eigene Wahl gilt der Standard-Anbieter (außer bei der Foto-Prüfung).</MudText>
|
||||
@foreach (var purpose in AiRules.SelectablePurposes)
|
||||
{
|
||||
var current = purpose;
|
||||
<MudCheckBox T="bool" Value="_purposes.HasFlag(current)" ValueChanged="on => SetPurpose(current, on)"
|
||||
Label="@AiRules.PurposeName(current)" Class="@($"fp-ai-purpose fp-ai-purpose-{(int)current}")" />
|
||||
}
|
||||
|
||||
@if (HasError("Purposes"))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error" role="alert">@ErrorFor("Purposes")</MudText>
|
||||
}
|
||||
</div>
|
||||
</MudStack>
|
||||
</EditForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel" Disabled="_busy">Abbrechen</MudButton>
|
||||
<MudButton ButtonType="ButtonType.Submit" form="ai-provider-form" Variant="Variant.Filled" Color="Color.Primary" Disabled="_busy" Class="fp-dialog-save">Speichern</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance? Dialog { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public AiProviderDto? Provider { get; set; }
|
||||
|
||||
private string _name = string.Empty;
|
||||
private AiProviderType _type = AiProviderType.Ollama;
|
||||
private string? _baseUrl;
|
||||
private string _model = string.Empty;
|
||||
private string? _apiKey;
|
||||
private bool _clearKey;
|
||||
private int _timeout = AiRules.DefaultTimeoutSeconds;
|
||||
private int _limit = AiRules.DefaultDailyLimit;
|
||||
private bool _active = true;
|
||||
private bool _allowImages;
|
||||
private AiPurpose _purposes;
|
||||
private Dictionary<string, string> _errors = new(StringComparer.OrdinalIgnoreCase);
|
||||
private string? _generalError;
|
||||
private bool _busy;
|
||||
|
||||
private AiProviderTypeInfo TypeInfo => AiRules.Info(_type);
|
||||
|
||||
private bool IsHomeNetwork => AiRules.NormalizeBaseUrl(_type, _baseUrl).Url is { } url && AiRules.IsHomeNetwork(url);
|
||||
|
||||
private string UrlHint => _type == AiProviderType.OpenAiCompatible
|
||||
? "Basisadresse ohne /chat/completions, z. B. https://api.perplexity.ai"
|
||||
: "Leer lassen für die Voreinstellung.";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Provider is not { } p)
|
||||
{
|
||||
_model = TypeInfo.DefaultModel;
|
||||
return;
|
||||
}
|
||||
|
||||
(_name, _type, _baseUrl, _model, _timeout, _limit, _active, _allowImages, _purposes) =
|
||||
(p.Name, p.Type, p.BaseUrl, p.Model, p.TimeoutSeconds, p.DailyLimitPerUser, p.IsActive, p.AllowImages, p.Purposes);
|
||||
}
|
||||
|
||||
private void OnTypeChanged(AiProviderType type)
|
||||
{
|
||||
// Voreinstellungen nur übernehmen, solange der Benutzer nichts Eigenes eingetragen hat.
|
||||
var oldInfo = TypeInfo;
|
||||
_type = type;
|
||||
if (string.IsNullOrWhiteSpace(_model) || _model == oldInfo.DefaultModel)
|
||||
{
|
||||
_model = TypeInfo.DefaultModel;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(_baseUrl) || _baseUrl == oldInfo.DefaultBaseUrl)
|
||||
{
|
||||
_baseUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPurpose(AiPurpose purpose, bool on) => _purposes = on ? _purposes | purpose : _purposes & ~purpose;
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
_generalError = null;
|
||||
var request = new SaveAiProviderRequest(
|
||||
_name.Trim(), _type, string.IsNullOrWhiteSpace(_baseUrl) ? null : _baseUrl.Trim(), _model.Trim(),
|
||||
string.IsNullOrEmpty(_apiKey) ? null : _apiKey.Trim(), _clearKey, _timeout, _active, _allowImages, _purposes, _limit, Provider?.Version);
|
||||
|
||||
var validation = new SaveAiProviderRequestValidator().Validate(request);
|
||||
_errors = validation.Errors
|
||||
.GroupBy(e => e.PropertyName)
|
||||
.ToDictionary(g => g.Key, g => string.Join(" ", g.Select(e => e.ErrorMessage)), StringComparer.OrdinalIgnoreCase);
|
||||
if (!validation.IsValid)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_busy = true;
|
||||
var result = Provider is null ? await Api.CreateProviderAsync(request) : await Api.UpdateProviderAsync(Provider.Id, request);
|
||||
_busy = false;
|
||||
if (result.Problem is { } problem)
|
||||
{
|
||||
_errors = (problem.Errors ?? [])
|
||||
.Where(e => e.Key.Length > 0)
|
||||
.ToDictionary(e => e.Key, e => string.Join(" ", e.Value), StringComparer.OrdinalIgnoreCase);
|
||||
if (_errors.Count == 0)
|
||||
{
|
||||
_generalError = problem.Message;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Dialog?.Close(DialogResult.Ok(result.Value));
|
||||
}
|
||||
|
||||
private bool HasError(string field) => _errors.ContainsKey(field);
|
||||
|
||||
private string? ErrorFor(string field) => _errors.GetValueOrDefault(field);
|
||||
|
||||
private void Cancel() => Dialog?.Cancel();
|
||||
}
|
||||
@@ -18,4 +18,7 @@
|
||||
<MudTabPanel Text="Kalender" Icon="@Icons.Material.Filled.EditCalendar">
|
||||
<CalendarManagement />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="KI" Icon="@Icons.Material.Filled.AutoAwesome">
|
||||
<AiManagement />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
@@ -33,6 +33,7 @@ builder.Services.AddScoped<ITasksApi, TasksApi>();
|
||||
builder.Services.AddScoped<ICalendarApi, CalendarApi>();
|
||||
builder.Services.AddScoped<IDashboardApi, DashboardApi>();
|
||||
builder.Services.AddScoped<IChatApi, ChatApi>();
|
||||
builder.Services.AddScoped<IAiApi, AiApi>();
|
||||
builder.Services.AddScoped<IPushService, PushService>();
|
||||
builder.Services.AddScoped<ILocalStore, BrowserLocalStore>();
|
||||
builder.Services.AddScoped<ILiveConnection, LiveConnection>();
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Net.Http.Json;
|
||||
using FamilyPlanner.Shared.Ai;
|
||||
|
||||
namespace FamilyPlanner.Web.Client.Services;
|
||||
|
||||
public interface IAiApi
|
||||
{
|
||||
Task<ApiResult<List<AiProviderDto>>> GetProvidersAsync(CancellationToken ct = default);
|
||||
|
||||
Task<ApiResult<AiProviderDto>> CreateProviderAsync(SaveAiProviderRequest request, CancellationToken ct = default);
|
||||
|
||||
/// <summary>Bei 409 (veraltete Version) enthält <see cref="ApiResult{T}.Problem"/> die Meldung des Servers.</summary>
|
||||
Task<ApiResult<AiProviderDto>> UpdateProviderAsync(Guid id, SaveAiProviderRequest request, CancellationToken ct = default);
|
||||
|
||||
Task<ApiResult<bool>> DeleteProviderAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
Task<ApiResult<AiTestResultDto>> TestProviderAsync(Guid id, CancellationToken ct = default);
|
||||
|
||||
Task<ApiResult<AiUsageOverviewDto>> GetUsageAsync(CancellationToken ct = default);
|
||||
|
||||
Task<ApiResult<AiStatusDto>> GetStatusAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class AiApi(HttpClient http) : IAiApi
|
||||
{
|
||||
public Task<ApiResult<List<AiProviderDto>>> GetProvidersAsync(CancellationToken ct = default) =>
|
||||
Send<List<AiProviderDto>>(() => http.GetAsync(AiApiRoutes.Providers, ct), ct);
|
||||
|
||||
public Task<ApiResult<AiProviderDto>> CreateProviderAsync(SaveAiProviderRequest request, CancellationToken ct = default) =>
|
||||
Send<AiProviderDto>(() => http.PostAsJsonAsync(AiApiRoutes.Providers, request, ct), ct);
|
||||
|
||||
public Task<ApiResult<AiProviderDto>> UpdateProviderAsync(Guid id, SaveAiProviderRequest request, CancellationToken ct = default) =>
|
||||
Send<AiProviderDto>(() => http.PutAsJsonAsync($"{AiApiRoutes.Providers}/{id}", request, ct), ct);
|
||||
|
||||
public Task<ApiResult<bool>> DeleteProviderAsync(Guid id, CancellationToken ct = default) =>
|
||||
ApiCall.SendAsync(() => http.DeleteAsync($"{AiApiRoutes.Providers}/{id}", ct), ct);
|
||||
|
||||
public Task<ApiResult<AiTestResultDto>> TestProviderAsync(Guid id, CancellationToken ct = default) =>
|
||||
Send<AiTestResultDto>(() => http.PostAsync($"{AiApiRoutes.Providers}/{id}/test", null, ct), ct);
|
||||
|
||||
public Task<ApiResult<AiUsageOverviewDto>> GetUsageAsync(CancellationToken ct = default) =>
|
||||
Send<AiUsageOverviewDto>(() => http.GetAsync(AiApiRoutes.Usage, ct), ct);
|
||||
|
||||
public Task<ApiResult<AiStatusDto>> GetStatusAsync(CancellationToken ct = default) =>
|
||||
Send<AiStatusDto>(() => http.GetAsync(AiApiRoutes.Status, ct), ct);
|
||||
|
||||
/// <summary>Wie <see cref="ApiCall"/>, aber 409 liefert die Fehlermeldung des Servers (kein aktueller Stand im Body).</summary>
|
||||
private static async Task<ApiResult<T>> Send<T>(Func<Task<HttpResponseMessage>> send, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var response = await send();
|
||||
return response.IsSuccessStatusCode
|
||||
? ApiResult<T>.Ok(await response.Content.ReadFromJsonAsync<T>(ct))
|
||||
: ApiResult<T>.Fail(await ApiClient.ReadProblemAsync(response, ct));
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
return ApiResult<T>.Fail(ApiProblem.Offline);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user