feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:ClawdDotNet.Desktop"
|
||||
x:Class="ClawdDotNet.Desktop.App"
|
||||
RequestedThemeVariant="Default">
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator />
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||
<StyleInclude Source="avares://ClawdDotNet/Styles/Shell.axaml" />
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
@@ -0,0 +1,213 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Desktop.Services;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
using ClawdDotNet.Desktop.Views;
|
||||
|
||||
namespace ClawdDotNet.Desktop;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private AppHost? _host;
|
||||
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
// Erst beenden, wenn wir es sagen: Zwischen Instanzauswahl und Hauptfenster
|
||||
// ist kurz gar kein Fenster offen. Mit OnLastWindowClose würde die Anwendung
|
||||
// in genau dieser Lücke aussteigen.
|
||||
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||
|
||||
desktop.ShutdownRequested += async (_, _) =>
|
||||
{
|
||||
if (_host is not null) await _host.DisposeAsync();
|
||||
};
|
||||
|
||||
// Nicht abwarten: OnFrameworkInitializationCompleted muss zurückkehren,
|
||||
// damit die Nachrichtenschleife anläuft — sonst gäbe es keinen Faden, auf
|
||||
// dem die Fenster des Startvorgangs überhaupt erscheinen könnten.
|
||||
_ = StartAsync(desktop);
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
|
||||
private async Task StartAsync(IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var result = await AppHost.StartAsync(new AppHost.Callbacks
|
||||
{
|
||||
SelectInstance = SelectInstanceAsync,
|
||||
License = new AvaloniaLicensePrompt(),
|
||||
TelegramLogin = prompt => AskAsync("Telegram Verifizierung", prompt),
|
||||
Telegram2FA = () => AskAsync("Telegram 2FA", "Bitte 2FA-Passwort eingeben:")
|
||||
});
|
||||
|
||||
if (result.Error is { } error)
|
||||
{
|
||||
await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Fehler", error);
|
||||
desktop.Shutdown(1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Host is null)
|
||||
{
|
||||
// Abbruch durch den Benutzer oder fehlende Lizenz — beides ist bereits
|
||||
// erklärt worden, hier kommt keine weitere Meldung hinterher.
|
||||
desktop.Shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
_host = result.Host;
|
||||
|
||||
HookErrorReporting(_host);
|
||||
HookLicenseWatch(_host, desktop);
|
||||
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = new MainWindowViewModel(_host)
|
||||
};
|
||||
|
||||
desktop.MainWindow.Show();
|
||||
desktop.MainWindow.Closed += (_, _) => desktop.Shutdown();
|
||||
|
||||
await ShowUpdateNoticeAsync(_host);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Meldet ungefangene Ausnahmen an den Fehler-Stream des Deploymentcenters.
|
||||
///
|
||||
/// <para>Erst hier verdrahtet, nicht in <c>Main</c>: Vor dem Aufbau gibt es weder
|
||||
/// Einstellungen noch Token, und ohne die wäre der Meldeweg ohnehin der Leerlauf.
|
||||
/// Die Kehrseite ist bewusst in Kauf genommen — ein Absturz <em>während</em> des
|
||||
/// Starts erreicht das Deploymentcenter nicht, steht aber im Protokoll.</para>
|
||||
/// </summary>
|
||||
private static void HookErrorReporting(AppHost host)
|
||||
{
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
||||
{
|
||||
if (args.ExceptionObject is Exception ex)
|
||||
{
|
||||
// Der Prozess endet gleich: kurze Frist, dann weiterlaufen lassen.
|
||||
host.Errors.ReportAsync(ex, fatal: args.IsTerminating)
|
||||
.Wait(TimeSpan.FromSeconds(3));
|
||||
}
|
||||
};
|
||||
|
||||
TaskScheduler.UnobservedTaskException += (_, args) =>
|
||||
{
|
||||
_ = host.Errors.ReportAsync(args.Exception, fatal: false);
|
||||
|
||||
// Ohne Observe reißt eine unbeobachtete Ausnahme in manchen Konfigurationen
|
||||
// den Prozess mit — und das wäre eine Nebenwirkung des Meldens.
|
||||
args.SetObserved();
|
||||
};
|
||||
|
||||
Dispatcher.UIThread.UnhandledException += (_, args) =>
|
||||
{
|
||||
_ = host.Errors.ReportAsync(args.Exception, fatal: false);
|
||||
|
||||
// Ein Fehler in einem Ereignisbehandler soll die Oberfläche nicht beenden.
|
||||
args.Handled = true;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Ein Widerruf beendet die Anwendung, ohne Beenden-Rückfrage.</summary>
|
||||
private static void HookLicenseWatch(AppHost host, IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
if (host.LicenseWatch is null) return;
|
||||
|
||||
host.LicenseWatch.Revoked += async message =>
|
||||
{
|
||||
await new AvaloniaLicensePrompt().ShowErrorAsync("ClawdDotNet – Lizenz", message);
|
||||
await Dispatcher.UIThread.InvokeAsync(() => desktop.Shutdown(2));
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hinweis auf ein verfügbares Update. Bewusst nur ein Hinweis: Wann aktualisiert
|
||||
/// wird, entscheidet der Benutzer — eine Anwendung, die sich beim Start selbst
|
||||
/// beendet, um sich zu erneuern, ist genau dann im Weg, wenn man sie braucht.
|
||||
/// </summary>
|
||||
private static async Task ShowUpdateNoticeAsync(AppHost host)
|
||||
{
|
||||
if (host.Deploymentcenter is null) return;
|
||||
|
||||
// Die Prüfung läuft nebenher; kurz Zeit geben, dann aufgeben.
|
||||
for (var waited = 0; host.Deploymentcenter.Update is null && waited < 10; waited++)
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
|
||||
if (host.Deploymentcenter.Update is not { IsAvailable: true } update) return;
|
||||
|
||||
await new AvaloniaLicensePrompt().ShowInfoAsync(
|
||||
update.IsCritical ? "ClawdDotNet – Wichtiges Update" : "ClawdDotNet – Update",
|
||||
$"Version {update.LatestVersion} ist verfügbar."
|
||||
+ (string.IsNullOrWhiteSpace(update.ReleaseNotes) ? "" : $"\n\n{update.ReleaseNotes}"));
|
||||
}
|
||||
|
||||
private static async Task<string?> SelectInstanceAsync(InstanceDirectoryManager directories)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var viewModel = new InstancePickerViewModel(directories);
|
||||
var window = new InstancePickerWindow { DataContext = viewModel };
|
||||
|
||||
// Schließt der Benutzer das Fenster, gilt das als Abbruch — sonst wartete
|
||||
// der Start für immer auf eine Auswahl, die nie kommt.
|
||||
window.Closed += (_, _) => viewModel.CancelCommand.Execute(null);
|
||||
|
||||
window.Show();
|
||||
|
||||
var path = await viewModel.Result;
|
||||
window.Close();
|
||||
|
||||
return path;
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Einzeilige Abfrage — ersetzt <c>Microsoft.VisualBasic.Interaction.InputBox</c>,
|
||||
/// das die Telegram-Anmeldung an Windows band.
|
||||
/// </summary>
|
||||
private static async Task<string> AskAsync(string title, string prompt)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var completion = new TaskCompletionSource<string>();
|
||||
|
||||
var input = new TextBox();
|
||||
var ok = new Button { Content = "OK", IsDefault = true };
|
||||
|
||||
var window = new Window
|
||||
{
|
||||
Title = title,
|
||||
Width = 420,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
CanResize = false,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Content = new StackPanel
|
||||
{
|
||||
Margin = new Thickness(20),
|
||||
Spacing = 12,
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = prompt, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
|
||||
input,
|
||||
ok
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ok.Click += (_, _) => window.Close();
|
||||
window.Closed += (_, _) => completion.TrySetResult(input.Text?.Trim() ?? "");
|
||||
|
||||
window.Show();
|
||||
input.Focus();
|
||||
|
||||
return await completion.Task;
|
||||
});
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -0,0 +1,38 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<RootNamespace>ClawdDotNet.Desktop</RootNamespace>
|
||||
<AssemblyName>ClawdDotNet</AssemblyName>
|
||||
<ApplicationIcon>Assets\app.ico</ApplicationIcon>
|
||||
|
||||
<!-- Avalonia legt AXAML-Dateien selbst als AvaloniaResource an; die Standard-Globs
|
||||
wuerden sie zusaetzlich als None einsammeln. -->
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
|
||||
<!-- Kein Konsolenfenster unter Windows, aber unter Linux ein normaler Prozess.
|
||||
WinExe verhaelt sich dort ohnehin wie Exe. -->
|
||||
<BuiltInComInteropSupport>false</BuiltInComInteropSupport>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.1.1" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.1" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.1" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.1" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="12.1.1" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ClawdDotNet.App\ClawdDotNet.App.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Färbt den Hintergrund einer Chat-Sprechblase für Benutzer-Nachrichten.
|
||||
/// </summary>
|
||||
public sealed class ChatBubbleBrushConverter : IValueConverter
|
||||
{
|
||||
public static readonly ChatBubbleBrushConverter Instance = new();
|
||||
|
||||
private static readonly IBrush UserBubble = new SolidColorBrush(Color.FromArgb(0x28, 0x00, 0x78, 0xD4));
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool isUser && isUser)
|
||||
return UserBubble;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException("Nur zur Anzeige.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Richtet Chat-Sprechblasen aus: Rechts für den Benutzer, links für den Agenten.
|
||||
/// </summary>
|
||||
public sealed class ChatAlignmentConverter : IValueConverter
|
||||
{
|
||||
public static readonly ChatAlignmentConverter Instance = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is bool isUser && isUser)
|
||||
return HorizontalAlignment.Right;
|
||||
|
||||
return HorizontalAlignment.Left;
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException("Nur zur Anzeige.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prüft, ob ein Tool-Name mit dem ConverterParameter übereinstimmt.
|
||||
/// </summary>
|
||||
public sealed class ToolMatchConverter : IValueConverter
|
||||
{
|
||||
public static readonly ToolMatchConverter Instance = new();
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string toolName && parameter is string targetTool)
|
||||
return toolName.Equals(targetTool, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException("Nur zur Anzeige.");
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Färbt eine Logzeile nach ihrer Stufe.
|
||||
///
|
||||
/// Bewusst nur Warnung und Fehler hervorgehoben — alles einzufärben macht die Ansicht
|
||||
/// unruhig und lenkt von genau den Zeilen ab, die auffallen sollen. Der Rest behält die
|
||||
/// Vordergrundfarbe des Themas und funktioniert damit hell wie dunkel.
|
||||
/// </summary>
|
||||
public sealed class LogLevelBrushConverter : IValueConverter
|
||||
{
|
||||
public static readonly LogLevelBrushConverter Instance = new();
|
||||
|
||||
private static readonly IBrush Error = new SolidColorBrush(Color.FromRgb(0xE8, 0x4B, 0x4B));
|
||||
private static readonly IBrush Warn = new SolidColorBrush(Color.FromRgb(0xD8, 0x9C, 0x2A));
|
||||
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> value as string switch
|
||||
{
|
||||
"ERR" or "FTL" => Error,
|
||||
"WRN" => Warn,
|
||||
// null heißt: Bindung greift nicht, das Steuerelement behält seine eigene
|
||||
// Farbe aus dem Thema.
|
||||
_ => null
|
||||
};
|
||||
|
||||
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException("Nur zur Anzeige.");
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Avalonia;
|
||||
|
||||
namespace ClawdDotNet.Desktop;
|
||||
|
||||
internal static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// Einstiegspunkt der Oberfläche.
|
||||
///
|
||||
/// Bewusst schlank: Hier wird nur Avalonia hochgefahren. Alles Fachliche —
|
||||
/// Einstellungen, Instanzauswahl, Engine, Scanner, Watchdog — baut
|
||||
/// <see cref="App"/> über die Anwendungsschicht auf, damit derselbe Aufbau später
|
||||
/// auch ohne Fenster laufen kann.
|
||||
///
|
||||
/// Kein <c>[STAThread]</c> mehr: Das war eine COM-Anforderung von WinForms. Avalonia
|
||||
/// braucht es nicht, und unter Linux hätte es ohnehin keine Bedeutung.
|
||||
/// </summary>
|
||||
public static int Main(string[] args) => BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
|
||||
/// <summary>
|
||||
/// Auch vom Vorschau-Werkzeug des Editors aufgerufen — deshalb öffentlich und
|
||||
/// getrennt von <see cref="Main"/>.
|
||||
/// </summary>
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
// Inter wird mitgeliefert, statt sich auf Systemschriften zu verlassen:
|
||||
// Ein schlankes Linux-Abbild hat oft gar keine, und dann bleibt die
|
||||
// Oberfläche leer.
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
using ClawdDotNet.Desktop.Views;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Lizenzabfrage über Fenster.
|
||||
///
|
||||
/// Der Aufbau läuft auf einem Hintergrundfaden — Avalonia besteht aber darauf, dass
|
||||
/// Fenster auf dem Oberflächenfaden entstehen. Deshalb geht hier alles über
|
||||
/// <see cref="Dispatcher.UIThread"/>. Das ist der Grund, warum
|
||||
/// <see cref="ILicensePrompt"/> durchgehend asynchron ist: Die WinForms-Fassung konnte
|
||||
/// <c>ShowDialog</c> einfach blockierend aufrufen, weil sie ohnehin auf dem
|
||||
/// Oberflächenfaden lief.
|
||||
/// </summary>
|
||||
public sealed class AvaloniaLicensePrompt : ILicensePrompt
|
||||
{
|
||||
public async Task<string?> RequestKeyAsync(string hardwareId, string? problem, string? currentKey)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var viewModel = new LicenseViewModel(hardwareId, problem, currentKey);
|
||||
var window = new LicenseWindow { DataContext = viewModel };
|
||||
|
||||
window.Show();
|
||||
|
||||
var key = await viewModel.Result;
|
||||
window.Close();
|
||||
|
||||
return key;
|
||||
});
|
||||
|
||||
public Task ShowInfoAsync(string title, string message)
|
||||
=> ShowMessageAsync(title, message);
|
||||
|
||||
public Task ShowErrorAsync(string title, string message)
|
||||
=> ShowMessageAsync(title, message);
|
||||
|
||||
/// <summary>
|
||||
/// Ein Meldungsfenster.
|
||||
///
|
||||
/// Avalonia bringt kein <c>MessageBox</c> mit — bewusst, weil es auf allen
|
||||
/// Plattformen anders aussähe. Ein schlichtes Fenster ist hier ausreichend und
|
||||
/// erspart uns eine weitere Abhängigkeit für drei Aufrufstellen.
|
||||
/// </summary>
|
||||
private static async Task ShowMessageAsync(string title, string message)
|
||||
=> await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var completion = new TaskCompletionSource();
|
||||
|
||||
var button = new Button
|
||||
{
|
||||
Content = "OK",
|
||||
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right,
|
||||
IsDefault = true
|
||||
};
|
||||
|
||||
var window = new Window
|
||||
{
|
||||
Title = title,
|
||||
Width = 460,
|
||||
SizeToContent = SizeToContent.Height,
|
||||
CanResize = false,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterScreen,
|
||||
Content = new StackPanel
|
||||
{
|
||||
Margin = new Avalonia.Thickness(20),
|
||||
Spacing = 16,
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = message, TextWrapping = Avalonia.Media.TextWrapping.Wrap },
|
||||
button
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
button.Click += (_, _) => window.Close();
|
||||
window.Closed += (_, _) => completion.TrySetResult();
|
||||
|
||||
window.Show();
|
||||
await completion.Task;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!--
|
||||
Gemeinsame Anmutung der Oberflaeche.
|
||||
|
||||
Bewusst wenige, benannte Regeln statt Formatierung an jedem Steuerelement: Die
|
||||
WinForms-Fassung hatte Groessen, Abstaende und Schriften ueber 130 Stellen in den
|
||||
Designer-Dateien verteilt: Jede Aenderung war eine Suche.
|
||||
-->
|
||||
|
||||
<Style Selector="TextBlock.heading">
|
||||
<Setter Property="FontSize" Value="16" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.caption">
|
||||
<Setter Property="Opacity" Value="0.7" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
|
||||
<!-- Werkzeugleiste ueber einer Ansicht -->
|
||||
<Style Selector="StackPanel.toolbar">
|
||||
<Setter Property="Orientation" Value="Horizontal" />
|
||||
<Setter Property="Spacing" Value="6" />
|
||||
<Setter Property="Margin" Value="8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.card">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Padding" Value="12" />
|
||||
</Style>
|
||||
|
||||
<!-- Statusleiste am unteren Rand -->
|
||||
<Style Selector="Border.statusbar">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource SystemControlForegroundBaseLowBrush}" />
|
||||
<Setter Property="BorderThickness" Value="0,1,0,0" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
@@ -0,0 +1,40 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop;
|
||||
|
||||
/// <summary>
|
||||
/// Findet zu einem Ansichtsmodell die passende Ansicht über die Namenskonvention
|
||||
/// <c>…ViewModels.FooViewModel</c> → <c>…Views.FooView</c>.
|
||||
///
|
||||
/// Das erspart es, jede Zuordnung von Hand als <c>DataTemplate</c> einzutragen — bei
|
||||
/// den sieben Bereichen und ihren Unteransichten wären das schnell dreißig Einträge,
|
||||
/// die man beim Umbenennen übersieht.
|
||||
///
|
||||
/// Passt nichts, erscheint der gesuchte Typname im Fenster statt einer leeren Fläche.
|
||||
/// Beim schrittweisen Umbau ist eine sichtbare Fehlstelle mehr wert als ein stiller
|
||||
/// weißer Bereich.
|
||||
/// </summary>
|
||||
public sealed class ViewLocator : IDataTemplate
|
||||
{
|
||||
public Control Build(object? data)
|
||||
{
|
||||
if (data is null)
|
||||
return new TextBlock { Text = "(kein Inhalt)" };
|
||||
|
||||
var viewModelName = data.GetType().FullName!;
|
||||
var viewName = viewModelName
|
||||
.Replace(".ViewModels.", ".Views.", StringComparison.Ordinal)
|
||||
.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||
|
||||
var type = Type.GetType(viewName);
|
||||
|
||||
if (type is null)
|
||||
return new TextBlock { Text = $"Ansicht nicht gefunden: {viewName}" };
|
||||
|
||||
return (Control)Activator.CreateInstance(type)!;
|
||||
}
|
||||
|
||||
public bool Match(object? data) => data is ViewModelBase;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record AddAgentResultData(
|
||||
string AgentId,
|
||||
string DisplayName,
|
||||
string Model,
|
||||
string Description
|
||||
);
|
||||
|
||||
public sealed partial class AddAgentViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _agentId = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _displayName = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedModel = "anthropic/claude-3-5-sonnet";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _description = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = "";
|
||||
|
||||
public List<string> AvailableModels { get; } =
|
||||
[
|
||||
"anthropic/claude-3-5-sonnet",
|
||||
"anthropic/claude-3-opus",
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"google/gemini-2.5-flash",
|
||||
"google/gemini-2.5-pro",
|
||||
"deepseek/deepseek-r1"
|
||||
];
|
||||
|
||||
public AddAgentResultData? Result { get; private set; }
|
||||
|
||||
public event Action? CloseRequested;
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(AgentId))
|
||||
{
|
||||
StatusMessage = "Bitte eine Agenten-ID eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(DisplayName))
|
||||
{
|
||||
StatusMessage = "Bitte einen Anzeigenamen eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new AddAgentResultData(
|
||||
AgentId.Trim(),
|
||||
DisplayName.Trim(),
|
||||
SelectedModel,
|
||||
Description.Trim()
|
||||
);
|
||||
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result = null;
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using ClawdDotNet.Core.Config;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record AddJobResultData(
|
||||
string AgentId,
|
||||
bool IsToolJob,
|
||||
string ToolName,
|
||||
string JobTypeId,
|
||||
string CronExpression,
|
||||
string TaskMessage,
|
||||
bool RunOnStart
|
||||
);
|
||||
|
||||
public sealed partial class AddJobViewModel : ViewModelBase
|
||||
{
|
||||
public List<AgentConfig> AvailableAgents { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private AgentConfig? _selectedAgent;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isToolJob;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isAgentWakeup = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedToolName = "FileRW";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _jobTypeId = "PollJob";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _cronExpression = "0 * * * *";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _taskMessage = "Führe deine zugewiesenen Aufgaben aus.";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _runOnStart;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = "";
|
||||
|
||||
public List<string> AvailableTools { get; } =
|
||||
[
|
||||
"FileRW", "Telegram", "Mail", "Database", "FTP",
|
||||
"DirectAPI", "WebFetch", "WebMonitor", "AgentComm",
|
||||
"SocialMediaManager", "AgentSpawn", "AgentEditor", "Memory", "Taskboard"
|
||||
];
|
||||
|
||||
public AddJobResultData? Result { get; private set; }
|
||||
|
||||
public event Action? CloseRequested;
|
||||
|
||||
public AddJobViewModel() : this([]) { }
|
||||
|
||||
public AddJobViewModel(List<AgentConfig> agents)
|
||||
{
|
||||
AvailableAgents = agents;
|
||||
SelectedAgent = AvailableAgents.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnIsToolJobChanged(bool value)
|
||||
{
|
||||
IsAgentWakeup = !value;
|
||||
}
|
||||
|
||||
partial void OnIsAgentWakeupChanged(bool value)
|
||||
{
|
||||
IsToolJob = !value;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
if (SelectedAgent is null)
|
||||
{
|
||||
StatusMessage = "Bitte einen Agenten auswählen.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(CronExpression))
|
||||
{
|
||||
StatusMessage = "Bitte einen Cron-Ausdruck angeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new AddJobResultData(
|
||||
SelectedAgent.AgentId,
|
||||
IsToolJob,
|
||||
SelectedToolName,
|
||||
JobTypeId,
|
||||
CronExpression.Trim(),
|
||||
TaskMessage.Trim(),
|
||||
RunOnStart
|
||||
);
|
||||
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result = null;
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record AddServiceResultData(
|
||||
string ServiceName,
|
||||
string ServiceType,
|
||||
int ServicePort,
|
||||
string ServiceDescription
|
||||
);
|
||||
|
||||
public sealed partial class AddServiceViewModel : ViewModelBase
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _serviceName = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _serviceType = "HTTP";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _servicePort = 8080;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _serviceDescription = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = "";
|
||||
|
||||
public List<string> AvailableServiceTypes { get; } = ["HTTP", "TCP", "gRPC", "WebSocket", "Custom"];
|
||||
|
||||
public AddServiceResultData? Result { get; private set; }
|
||||
|
||||
public event Action? CloseRequested;
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ServiceName))
|
||||
{
|
||||
StatusMessage = "Bitte einen Dienstnamen eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (ServicePort <= 0 || ServicePort > 65535)
|
||||
{
|
||||
StatusMessage = "Gültigen Port zwischen 1 und 65535 eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new AddServiceResultData(
|
||||
ServiceName.Trim(),
|
||||
ServiceType,
|
||||
ServicePort,
|
||||
ServiceDescription.Trim()
|
||||
);
|
||||
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result = null;
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed partial class ToolItemViewModel : ObservableObject
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isEnabled;
|
||||
|
||||
public ToolItemViewModel(string name, bool isEnabled)
|
||||
{
|
||||
Name = name;
|
||||
_isEnabled = isEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class AgentItemViewModel : ObservableObject
|
||||
{
|
||||
public AgentConfig Config { get; }
|
||||
|
||||
public string AgentId => Config.AgentId;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _displayName;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _model;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _systemPrompt;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _identity;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _soul;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _description;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _promptCaching;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _maxToolResultChars;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _maxSteps;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _maxContextTokens;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _timeoutSeconds;
|
||||
|
||||
[ObservableProperty]
|
||||
private decimal _dailyCostUsd;
|
||||
|
||||
[ObservableProperty]
|
||||
private long _dailyTokens;
|
||||
|
||||
public ObservableCollection<ToolItemViewModel> AvailableTools { get; } = [];
|
||||
|
||||
public static readonly List<string> KnownTools =
|
||||
[
|
||||
"FileRW", "Telegram", "Mail", "Database", "FTP",
|
||||
"DirectAPI", "WebFetch", "WebMonitor", "AgentComm",
|
||||
"SocialMediaManager", "AgentSpawn", "AgentEditor", "Memory", "Taskboard"
|
||||
];
|
||||
|
||||
public AgentItemViewModel(AgentConfig config)
|
||||
{
|
||||
Config = config;
|
||||
_displayName = config.DisplayName;
|
||||
_model = config.Model;
|
||||
_systemPrompt = config.SystemPrompt;
|
||||
_identity = config.Identity;
|
||||
_soul = config.Soul;
|
||||
_description = config.Description;
|
||||
_promptCaching = config.PromptCaching;
|
||||
_maxToolResultChars = config.MaxToolResultChars;
|
||||
_maxSteps = config.LoopGuard.MaxSteps;
|
||||
_maxContextTokens = config.LoopGuard.MaxContextTokens;
|
||||
_timeoutSeconds = config.LoopGuard.TimeoutSeconds;
|
||||
_dailyCostUsd = config.Budget.DailyCostUsd;
|
||||
_dailyTokens = config.Budget.DailyTokens;
|
||||
|
||||
foreach (var tool in KnownTools)
|
||||
{
|
||||
var isEnabled = config.Tools.ContainsKey(tool);
|
||||
var item = new ToolItemViewModel(tool, isEnabled);
|
||||
item.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ToolItemViewModel.IsEnabled))
|
||||
{
|
||||
if (item.IsEnabled && !Config.Tools.ContainsKey(tool))
|
||||
Config.Tools[tool] = new Dictionary<string, object?>();
|
||||
else if (!item.IsEnabled)
|
||||
Config.Tools.Remove(tool);
|
||||
}
|
||||
};
|
||||
AvailableTools.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyChanges()
|
||||
{
|
||||
Config.DisplayName = DisplayName;
|
||||
Config.Model = Model;
|
||||
Config.SystemPrompt = SystemPrompt;
|
||||
Config.Identity = Identity;
|
||||
Config.Soul = Soul;
|
||||
Config.Description = Description;
|
||||
Config.PromptCaching = PromptCaching;
|
||||
Config.MaxToolResultChars = MaxToolResultChars;
|
||||
Config.LoopGuard.MaxSteps = MaxSteps;
|
||||
Config.LoopGuard.MaxContextTokens = MaxContextTokens;
|
||||
Config.LoopGuard.TimeoutSeconds = TimeoutSeconds;
|
||||
Config.Budget.DailyCostUsd = DailyCostUsd;
|
||||
Config.Budget.DailyTokens = DailyTokens;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die Agenten-Verwaltungsseite.
|
||||
/// </summary>
|
||||
public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly AppHost? _host;
|
||||
private readonly ILogger? _logger;
|
||||
|
||||
public ObservableCollection<AgentItemViewModel> Agents { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private AgentItemViewModel? _selectedAgent;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
public List<string> KnownModels { get; } =
|
||||
[
|
||||
"anthropic/claude-3-5-sonnet",
|
||||
"anthropic/claude-3-opus",
|
||||
"openai/gpt-4o",
|
||||
"openai/gpt-4o-mini",
|
||||
"google/gemini-2.5-flash",
|
||||
"google/gemini-2.5-pro",
|
||||
"deepseek/deepseek-r1"
|
||||
];
|
||||
|
||||
public List<string> CachingOptions { get; } = ["auto", "on", "off"];
|
||||
|
||||
public event Func<AddAgentViewModel, Task<AddAgentResultData?>>? RequestAddAgentDialog;
|
||||
public event Func<ToolSettingsViewModel, Task<Dictionary<string, object?>?>>? RequestToolSettingsDialog;
|
||||
|
||||
public AgentsPageViewModel() : this(null) { }
|
||||
|
||||
public AgentsPageViewModel(AppHost? host) : base("Agenten")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Agents");
|
||||
ReloadAgents();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ReloadAgents()
|
||||
{
|
||||
Agents.Clear();
|
||||
if (_host is null) return;
|
||||
|
||||
foreach (var agent in _host.Instance.Agents)
|
||||
{
|
||||
Agents.Add(new AgentItemViewModel(agent));
|
||||
}
|
||||
|
||||
SelectedAgent = Agents.FirstOrDefault();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddAgentAsync()
|
||||
{
|
||||
if (_host is null || RequestAddAgentDialog is null) return;
|
||||
|
||||
var vm = new AddAgentViewModel();
|
||||
var result = await RequestAddAgentDialog(vm);
|
||||
if (result is null) return;
|
||||
|
||||
if (_host.Instance.Agents.Any(a => a.AgentId.Equals(result.AgentId, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
StatusText = $"Ein Agent mit der ID '{result.AgentId}' existiert bereits.";
|
||||
return;
|
||||
}
|
||||
|
||||
var newConfig = new AgentConfig
|
||||
{
|
||||
AgentId = result.AgentId,
|
||||
DisplayName = result.DisplayName,
|
||||
Model = result.Model,
|
||||
Description = result.Description
|
||||
};
|
||||
|
||||
_host.Instance.Agents.Add(newConfig);
|
||||
SaveAgents();
|
||||
|
||||
var item = new AgentItemViewModel(newConfig);
|
||||
Agents.Add(item);
|
||||
SelectedAgent = item;
|
||||
|
||||
StatusText = $"Agent '{result.DisplayName}' angelegt.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveAgent()
|
||||
{
|
||||
if (_host is null || SelectedAgent is null) return;
|
||||
|
||||
var agentId = SelectedAgent.AgentId;
|
||||
_host.Instance.Agents.RemoveAll(a => a.AgentId == agentId);
|
||||
|
||||
SaveAgents();
|
||||
Agents.Remove(SelectedAgent);
|
||||
SelectedAgent = Agents.FirstOrDefault();
|
||||
|
||||
StatusText = $"Agent '{agentId}' entfernt.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfigureToolAsync(ToolItemViewModel? toolItem)
|
||||
{
|
||||
if (SelectedAgent is null || toolItem is null || RequestToolSettingsDialog is null) return;
|
||||
|
||||
var toolName = toolItem.Name;
|
||||
if (!SelectedAgent.Config.Tools.TryGetValue(toolName, out var config))
|
||||
{
|
||||
config = new Dictionary<string, object?>();
|
||||
SelectedAgent.Config.Tools[toolName] = config;
|
||||
toolItem.IsEnabled = true;
|
||||
}
|
||||
|
||||
var vm = new ToolSettingsViewModel(toolName, config);
|
||||
var newConfig = await RequestToolSettingsDialog(vm);
|
||||
|
||||
if (newConfig is not null)
|
||||
{
|
||||
SelectedAgent.Config.Tools[toolName] = newConfig;
|
||||
SaveAgents();
|
||||
StatusText = $"Einstellungen für Werkzeug '{toolName}' gespeichert.";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveAgents()
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
foreach (var agentVm in Agents)
|
||||
{
|
||||
agentVm.ApplyChanges();
|
||||
}
|
||||
|
||||
_host.Directories.SaveInstanceConfig(_host.InstancePath, _host.Instance);
|
||||
_logger?.LogInformation("Agenten-Konfiguration gespeichert.");
|
||||
StatusText = "Agenten-Konfiguration gespeichert.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Settings;
|
||||
using ClawdDotNet.Core.Backup;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record BackupItemViewModel(
|
||||
string FileName,
|
||||
DateTime LastWriteTime,
|
||||
string InstanceName,
|
||||
long SizeBytes,
|
||||
string SizeFormatted,
|
||||
bool HasSecrets,
|
||||
string SecretsInfo,
|
||||
string FullPath,
|
||||
bool IsReadable
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die Sicherungs- und Wiederherstellungsseite.
|
||||
/// </summary>
|
||||
public sealed partial class BackupPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly BackupService _service = new();
|
||||
private readonly AppHost? _host;
|
||||
private readonly SettingsManager? _settingsManager;
|
||||
private readonly ILogger? _logger;
|
||||
private readonly string _instanceDir;
|
||||
private readonly string _instanceName;
|
||||
private bool _loading;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _targetFolder = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _secretsExclude = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _secretsPassphrase;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _passphrase = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _passphraseRepeat = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _includeLogs;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _includeChatHistory = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoBackupEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _autoBackupTime = "03:00";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _keepCount = 10;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isBusy;
|
||||
|
||||
[ObservableProperty]
|
||||
private BackupItemViewModel? _selectedBackup;
|
||||
|
||||
public ObservableCollection<BackupItemViewModel> Backups { get; } = [];
|
||||
|
||||
public event Func<RestoreBackupViewModel, Task<RestoreResultData?>>? RequestRestoreDialog;
|
||||
|
||||
public BackupPageViewModel() : this(null) { }
|
||||
|
||||
public BackupPageViewModel(AppHost? host) : base("Sicherung")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
_instanceDir = host.InstancePath;
|
||||
_instanceName = host.Instance.InstanceName;
|
||||
_settingsManager = host.Settings;
|
||||
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Backup");
|
||||
|
||||
LoadSettings();
|
||||
RefreshBackupList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_instanceDir = "";
|
||||
_instanceName = "Entwurf";
|
||||
TargetFolder = @"C:\ClawdDotNet\Backups";
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnAutoBackupEnabledChanged(bool value) => SaveSettings();
|
||||
partial void OnAutoBackupTimeChanged(string value) => SaveSettings();
|
||||
partial void OnKeepCountChanged(int value) => SaveSettings();
|
||||
|
||||
private void LoadSettings()
|
||||
{
|
||||
if (_settingsManager is null) return;
|
||||
|
||||
_loading = true;
|
||||
try
|
||||
{
|
||||
var settings = _settingsManager.AppSettings;
|
||||
TargetFolder = Path.GetFullPath(settings.BackupDirectory);
|
||||
AutoBackupEnabled = settings.AutoBackupEnabled;
|
||||
KeepCount = Math.Clamp(settings.BackupKeepCount, 0, 999);
|
||||
|
||||
AutoBackupTime = TimeSpan.TryParse(settings.AutoBackupTime, out var time)
|
||||
? time.ToString(@"hh\:mm")
|
||||
: "03:00";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (_loading || _settingsManager is null) return;
|
||||
|
||||
var settings = _settingsManager.AppSettings;
|
||||
settings.BackupDirectory = TargetFolder.Trim();
|
||||
settings.AutoBackupEnabled = AutoBackupEnabled;
|
||||
settings.AutoBackupTime = AutoBackupTime.Trim();
|
||||
settings.BackupKeepCount = KeepCount;
|
||||
|
||||
_settingsManager.Save();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RefreshBackupList()
|
||||
{
|
||||
Backups.Clear();
|
||||
var folder = TargetFolder.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(folder) || !Directory.Exists(folder))
|
||||
return;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
var files = new DirectoryInfo(folder)
|
||||
.GetFiles("*.zip")
|
||||
.OrderByDescending(f => f.LastWriteTime)
|
||||
.ToList();
|
||||
|
||||
var list = new List<BackupItemViewModel>();
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var manifest = await _service.InspectAsync(file.FullName);
|
||||
var secretsInfo = manifest.HasSecrets
|
||||
? $"ja ({manifest.SecretCount}, Passphrase)"
|
||||
: "nein";
|
||||
|
||||
list.Add(new BackupItemViewModel(
|
||||
file.Name, file.LastWriteTime, manifest.InstanceName,
|
||||
file.Length, FormatSize(file.Length), manifest.HasSecrets, secretsInfo,
|
||||
file.FullName, true));
|
||||
}
|
||||
catch
|
||||
{
|
||||
list.Add(new BackupItemViewModel(
|
||||
file.Name, file.LastWriteTime, "—",
|
||||
file.Length, FormatSize(file.Length), false, "unlesbar",
|
||||
file.FullName, false));
|
||||
}
|
||||
}
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
Backups.Clear();
|
||||
foreach (var item in list)
|
||||
Backups.Add(item);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateBackupAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_instanceDir))
|
||||
{
|
||||
StatusText = "Keine gültige Instanz geladen.";
|
||||
return;
|
||||
}
|
||||
|
||||
var folder = TargetFolder.Trim();
|
||||
if (string.IsNullOrWhiteSpace(folder))
|
||||
{
|
||||
StatusText = "Bitte einen Zielordner angeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
BackupOptions options;
|
||||
if (SecretsPassphrase)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Passphrase))
|
||||
{
|
||||
StatusText = "Bitte eine Passphrase eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (Passphrase != PassphraseRepeat)
|
||||
{
|
||||
StatusText = "Die Passphrasen stimmen nicht überein.";
|
||||
return;
|
||||
}
|
||||
|
||||
options = new BackupOptions
|
||||
{
|
||||
Secrets = SecretMode.Passphrase,
|
||||
Passphrase = Passphrase,
|
||||
IncludeLogs = IncludeLogs,
|
||||
IncludeChatHistory = IncludeChatHistory
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
options = new BackupOptions
|
||||
{
|
||||
Secrets = SecretMode.Exclude,
|
||||
IncludeLogs = IncludeLogs,
|
||||
IncludeChatHistory = IncludeChatHistory
|
||||
};
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(folder);
|
||||
var fileName = BuildFileName();
|
||||
var zipPath = Path.Combine(folder, fileName);
|
||||
|
||||
IsBusy = true;
|
||||
StatusText = "Sicherung läuft…";
|
||||
|
||||
try
|
||||
{
|
||||
var result = await Task.Run(() => _service.CreateAsync(_instanceDir, zipPath, options));
|
||||
|
||||
StatusText = $"Fertig: {Path.GetFileName(result.ZipPath)} ({FormatSize(result.SizeBytes)})";
|
||||
_logger?.LogInformation("Sicherung erstellt: {Path} ({Size} Bytes)", result.ZipPath, result.SizeBytes);
|
||||
|
||||
Passphrase = "";
|
||||
PassphraseRepeat = "";
|
||||
|
||||
ApplyRotation(folder);
|
||||
RefreshBackupList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Fehler: {ex.Message}";
|
||||
_logger?.LogError(ex, "Sicherung fehlgeschlagen");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowSelectedInFolder()
|
||||
{
|
||||
if (SelectedBackup is null) return;
|
||||
SystemShell.RevealFile(SelectedBackup.FullPath);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void DeleteSelected()
|
||||
{
|
||||
if (SelectedBackup is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
File.Delete(SelectedBackup.FullPath);
|
||||
RefreshBackupList();
|
||||
StatusText = $"Gelöscht: {SelectedBackup.FileName}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Konnte nicht gelöscht werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RestoreSelectedAsync()
|
||||
{
|
||||
if (SelectedBackup is null || RequestRestoreDialog is null) return;
|
||||
|
||||
BackupManifest manifest;
|
||||
try
|
||||
{
|
||||
manifest = await _service.InspectAsync(SelectedBackup.FullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Sicherung unlesbar: {ex.Message}";
|
||||
return;
|
||||
}
|
||||
|
||||
var suggestedTarget = SuggestRestoreTarget(manifest);
|
||||
var vm = new RestoreBackupViewModel(manifest, suggestedTarget);
|
||||
|
||||
var resultData = await RequestRestoreDialog(vm);
|
||||
if (resultData is null) return;
|
||||
|
||||
IsBusy = true;
|
||||
StatusText = "Wiederherstellung läuft…";
|
||||
|
||||
try
|
||||
{
|
||||
var restoreOptions = new RestoreOptions
|
||||
{
|
||||
Passphrase = resultData.Passphrase,
|
||||
Overwrite = resultData.Overwrite
|
||||
};
|
||||
|
||||
var result = await Task.Run(() => _service.RestoreAsync(
|
||||
SelectedBackup.FullPath, resultData.TargetDirectory, restoreOptions));
|
||||
|
||||
StatusText = $"{result.Written.Count} Datei(en) wiederhergestellt nach {resultData.TargetDirectory}";
|
||||
_logger?.LogInformation("Wiederherstellung: {Written} geschrieben nach {Target}",
|
||||
result.Written.Count, resultData.TargetDirectory);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Wiederherstellung fehlgeschlagen: {ex.Message}";
|
||||
_logger?.LogError(ex, "Wiederherstellung fehlgeschlagen");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildFileName()
|
||||
{
|
||||
var name = string.IsNullOrWhiteSpace(_instanceName) ? "Instanz" : _instanceName;
|
||||
var safeName = string.Concat(name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
return $"backup_{safeName}_{DateTime.Now:yyyy-MM-dd_HHmm}.zip";
|
||||
}
|
||||
|
||||
private string SuggestRestoreTarget(BackupManifest manifest)
|
||||
{
|
||||
var parent = Directory.GetParent(_instanceDir)?.FullName ?? _instanceDir;
|
||||
var name = string.IsNullOrWhiteSpace(manifest.InstanceName) ? "Instanz" : manifest.InstanceName;
|
||||
var safeName = string.Concat(name.Select(c => Path.GetInvalidFileNameChars().Contains(c) ? '_' : c));
|
||||
return Path.Combine(parent, $"Instance-{safeName}_wiederhergestellt_{DateTime.Now:yyyyMMdd_HHmm}");
|
||||
}
|
||||
|
||||
private void ApplyRotation(string folder)
|
||||
{
|
||||
if (KeepCount <= 0 || !Directory.Exists(folder)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var prefix = $"backup_{_instanceName}_";
|
||||
var obsolete = new DirectoryInfo(folder)
|
||||
.GetFiles("*.zip")
|
||||
.Where(f => f.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(f => f.LastWriteTime)
|
||||
.Skip(KeepCount)
|
||||
.ToList();
|
||||
|
||||
foreach (var file in obsolete)
|
||||
{
|
||||
file.Delete();
|
||||
_logger?.LogInformation("Alte Sicherung rotiert: {Name}", file.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogWarning(ex, "Rotation der Sicherungen fehlgeschlagen");
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes) => bytes switch
|
||||
{
|
||||
< 1024 => $"{bytes} B",
|
||||
< 1024 * 1024 => $"{bytes / 1024.0:F1} KB",
|
||||
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
|
||||
_ => $"{bytes / 1024.0 / 1024 / 1024:F2} GB"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record ChatEntryItemViewModel(
|
||||
string Role,
|
||||
string Sender,
|
||||
string Text,
|
||||
DateTime Timestamp,
|
||||
bool IsUser
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die native Chat-Seite (Avalonia UI statt WebView2).
|
||||
/// </summary>
|
||||
public sealed partial class ChatPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly AppHost? _host;
|
||||
|
||||
public ObservableCollection<AgentConfig> AvailableAgents { get; } = [];
|
||||
public ObservableCollection<ChatEntryItemViewModel> ChatEntries { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private AgentConfig? _selectedAgent;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _inputPrompt = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isSending;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
public ChatPageViewModel() : this(null) { }
|
||||
|
||||
public ChatPageViewModel(AppHost? host) : base("Chat")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
foreach (var agent in host.Instance.Agents)
|
||||
AvailableAgents.Add(agent);
|
||||
|
||||
SelectedAgent = AvailableAgents.FirstOrDefault();
|
||||
|
||||
if (host.Engine is not null)
|
||||
{
|
||||
host.Engine.OnChatEntryAdded += OnEngineChatEntryAdded;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedAgentChanged(AgentConfig? value)
|
||||
{
|
||||
LoadChatHistory();
|
||||
}
|
||||
|
||||
private void LoadChatHistory()
|
||||
{
|
||||
ChatEntries.Clear();
|
||||
if (_host?.Engine is null || SelectedAgent is null) return;
|
||||
|
||||
var history = _host.Engine.GetChatHistory(SelectedAgent.AgentId);
|
||||
foreach (var entry in history)
|
||||
{
|
||||
var isUser = entry.Role.Equals("user", StringComparison.OrdinalIgnoreCase);
|
||||
ChatEntries.Add(new ChatEntryItemViewModel(
|
||||
entry.Role,
|
||||
isUser ? "Du" : SelectedAgent.DisplayName,
|
||||
entry.Content,
|
||||
entry.Timestamp,
|
||||
isUser
|
||||
));
|
||||
}
|
||||
|
||||
StatusText = $"Chat-Verlauf für '{SelectedAgent.DisplayName}' geladen ({ChatEntries.Count} Nachrichten).";
|
||||
}
|
||||
|
||||
private void OnEngineChatEntryAdded(string agentId, string role, string text, string? toolName)
|
||||
{
|
||||
if (SelectedAgent is null || SelectedAgent.AgentId != agentId) return;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
var isUser = role.Equals("user", StringComparison.OrdinalIgnoreCase);
|
||||
var sender = isUser ? "Du" : (string.IsNullOrWhiteSpace(toolName) ? SelectedAgent.DisplayName : $"Tool: {toolName}");
|
||||
|
||||
ChatEntries.Add(new ChatEntryItemViewModel(
|
||||
role, sender, text, DateTime.Now, isUser
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task SendMessageAsync()
|
||||
{
|
||||
if (_host?.Engine is null || SelectedAgent is null)
|
||||
{
|
||||
StatusText = "Engine nicht aktiv oder kein Agent ausgewählt.";
|
||||
return;
|
||||
}
|
||||
|
||||
var text = InputPrompt.Trim();
|
||||
if (string.IsNullOrWhiteSpace(text)) return;
|
||||
|
||||
InputPrompt = "";
|
||||
IsSending = true;
|
||||
StatusText = $"{SelectedAgent.DisplayName} denkt nach…";
|
||||
|
||||
try
|
||||
{
|
||||
var agent = SelectedAgent;
|
||||
var instanceId = _host.Instance.InstanceId;
|
||||
|
||||
var result = await Task.Run(() =>
|
||||
_host.Engine.ChatAsync(agent, text, instanceId, CancellationToken.None));
|
||||
|
||||
StatusText = $"{agent.DisplayName} fertig ({result.Status}, {result.TokensUsed:N0} Tokens).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSending = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Abort()
|
||||
{
|
||||
if (_host?.Engine is null || SelectedAgent is null) return;
|
||||
_host.Engine.AbortChat(SelectedAgent.AgentId);
|
||||
StatusText = "Chat abgebrochen.";
|
||||
IsSending = false;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearHistory()
|
||||
{
|
||||
if (_host?.Engine is null || SelectedAgent is null) return;
|
||||
_host.Engine.ClearChatHistory(SelectedAgent.AgentId);
|
||||
ChatEntries.Clear();
|
||||
StatusText = "Verlauf geleert.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using ClawdDotNet.App;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record AssemblyInfoItem(string ShortName, int BuildNumber, string BuildDate, string Changes);
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die Info-Seite (Version, Build-Informationen, Instanz-Details).
|
||||
/// </summary>
|
||||
public sealed partial class InfoPageViewModel : PageViewModel
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _appVersion = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _buildSummary = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _instanceName = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _instanceId = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _instancePath = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _logDirectory = "—";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _openRouterStatus = "—";
|
||||
|
||||
public ObservableCollection<AssemblyInfoItem> Assemblies { get; } = [];
|
||||
|
||||
public InfoPageViewModel() : this(null) { }
|
||||
|
||||
public InfoPageViewModel(AppHost? host) : base("Info")
|
||||
{
|
||||
AppVersion = AppHost.AppVersion;
|
||||
BuildSummary = AppHost.BuildSummary;
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
InstanceName = host.Instance.InstanceName;
|
||||
InstanceId = host.Instance.InstanceId;
|
||||
InstancePath = host.InstancePath;
|
||||
LogDirectory = host.LogDirectory;
|
||||
OpenRouterStatus = host.Engine is null
|
||||
? "Deaktiviert (kein API-Key hinterlegt)"
|
||||
: "Aktiv (AgentEngine bereit)";
|
||||
}
|
||||
else
|
||||
{
|
||||
InstanceName = "Entwurfsmodus";
|
||||
InstanceId = "00000000-0000-0000-0000-000000000000";
|
||||
InstancePath = @"C:\ClawdDotNet\Instances\Default";
|
||||
LogDirectory = @"C:\ClawdDotNet\Logs";
|
||||
OpenRouterStatus = "Entwurf";
|
||||
}
|
||||
|
||||
LoadAssemblyBuildInfos();
|
||||
}
|
||||
|
||||
private void LoadAssemblyBuildInfos()
|
||||
{
|
||||
Assemblies.Clear();
|
||||
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => a.GetName().Name?.StartsWith("ClawdDotNet") == true)
|
||||
.OrderBy(a => a.GetName().Name))
|
||||
{
|
||||
var name = asm.GetName().Name!;
|
||||
var shortName = name.Replace("ClawdDotNet.", "").Replace("Tools.", "");
|
||||
|
||||
var buildDate = asm.GetCustomAttributes(typeof(AssemblyMetadataAttribute), false)
|
||||
.OfType<AssemblyMetadataAttribute>()
|
||||
.FirstOrDefault(a => a.Key == "BuildDate")?.Value ?? "?";
|
||||
|
||||
var buildInfoType = asm.GetType(asm.GetName().Name + ".BuildInfo");
|
||||
var buildNum = buildInfoType?.GetField("Build")?.GetValue(null) as int? ?? 0;
|
||||
var changes = buildInfoType?.GetField("Changes")?.GetValue(null) as string ?? "—";
|
||||
|
||||
Assemblies.Add(new AssemblyInfoItem(shortName, buildNum, buildDate, changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClawdDotNet.App.Models;
|
||||
using ClawdDotNet.App.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Auswahl der Instanz beim Start — der erste Rückruf, den <c>AppHost</c> erfragt.
|
||||
///
|
||||
/// Die WinForms-Fassung mischte Datenzugriff, Spaltenformatierung und Meldungsfenster in
|
||||
/// einem Formular. Hier steht nur, <em>was</em> geschieht; das <em>wie</em> es aussieht
|
||||
/// liegt in der Ansicht, und Rückfragen laufen über <see cref="ConfirmError"/> statt über
|
||||
/// einen direkten Dialogaufruf — so bleibt das Ansichtsmodell ohne Fensterbezug prüfbar.
|
||||
/// </summary>
|
||||
public sealed partial class InstancePickerViewModel : ViewModelBase
|
||||
{
|
||||
private readonly InstanceDirectoryManager _directories;
|
||||
private readonly TaskCompletionSource<string?> _completion = new();
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(StartCommand))]
|
||||
private InstanceInfo? _selected;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _newInstanceName = "";
|
||||
|
||||
/// <summary>Fehlertext für die Ansicht; leer heißt: alles in Ordnung.</summary>
|
||||
[ObservableProperty]
|
||||
private string _errorText = "";
|
||||
|
||||
public ObservableCollection<InstanceInfo> Instances { get; } = [];
|
||||
|
||||
/// <summary>Der gewählte Pfad, oder <c>null</c> bei Abbruch.</summary>
|
||||
public Task<string?> Result => _completion.Task;
|
||||
|
||||
public string InstancesDirectory => _directories.InstancesDirectory;
|
||||
|
||||
public InstancePickerViewModel(InstanceDirectoryManager directories)
|
||||
{
|
||||
_directories = directories;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Refresh()
|
||||
{
|
||||
Instances.Clear();
|
||||
|
||||
foreach (var instance in _directories.ListInstances())
|
||||
Instances.Add(instance);
|
||||
|
||||
// Eine einzelne Instanz gleich vorwählen — der häufigste Fall, und ein Klick
|
||||
// weniger bei jedem Start.
|
||||
Selected ??= Instances.FirstOrDefault();
|
||||
}
|
||||
|
||||
private bool CanStart => Selected is not null;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanStart))]
|
||||
private void Start() => _completion.TrySetResult(Selected!.FolderPath);
|
||||
|
||||
/// <summary>Abbruch — der Aufrufer beendet die Anwendung.</summary>
|
||||
[RelayCommand]
|
||||
private void Cancel() => _completion.TrySetResult(null);
|
||||
|
||||
[RelayCommand]
|
||||
private void Create()
|
||||
{
|
||||
var name = NewInstanceName.Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
ErrorText = "Bitte einen Instanznamen eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_directories.CreateInstance(name);
|
||||
NewInstanceName = "";
|
||||
ErrorText = "";
|
||||
Refresh();
|
||||
|
||||
// Die frisch angelegte Instanz vorwählen — wer sie erstellt, will sie starten.
|
||||
Selected = Instances.FirstOrDefault(i =>
|
||||
string.Equals(i.InstanceName, name, StringComparison.Ordinal));
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
|
||||
or ArgumentException)
|
||||
{
|
||||
ErrorText = $"Instanz konnte nicht angelegt werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wird gesetzt, wenn ein Fehler von außen kommt (etwa eine unlesbare
|
||||
/// Instanzkonfiguration), damit das Fenster offen bleiben und eine andere Wahl
|
||||
/// zulassen kann.
|
||||
/// </summary>
|
||||
public void ConfirmError(string message) => ErrorText = message;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
/// <summary>Eingabe eines Lizenzschlüssels. Siehe <c>AvaloniaLicensePrompt</c>.</summary>
|
||||
public sealed partial class LicenseViewModel : ViewModelBase
|
||||
{
|
||||
private readonly TaskCompletionSource<string?> _completion = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private string _key;
|
||||
|
||||
public string HardwareId { get; }
|
||||
|
||||
/// <summary>Warum der bisherige Schlüssel nicht taugt; leer beim ersten Fragen.</summary>
|
||||
public string Problem { get; }
|
||||
|
||||
/// <summary>Der eingegebene Schlüssel, oder <c>null</c> bei Abbruch.</summary>
|
||||
public Task<string?> Result => _completion.Task;
|
||||
|
||||
public LicenseViewModel(string hardwareId, string? problem, string? currentKey)
|
||||
{
|
||||
HardwareId = hardwareId;
|
||||
Problem = problem ?? "";
|
||||
_key = currentKey ?? "";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
var value = Key.Trim();
|
||||
|
||||
// Leere Eingabe ist kein Abbruch — sonst schließt sich das Fenster, wenn jemand
|
||||
// versehentlich die Eingabetaste drückt.
|
||||
if (value.Length == 0) return;
|
||||
|
||||
_completion.TrySetResult(value);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel() => _completion.TrySetResult(null);
|
||||
|
||||
/// <summary>Schließt der Benutzer das Fenster, gilt das als Abbruch.</summary>
|
||||
public void CancelIfPending() => _completion.TrySetResult(null);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Live-Ansicht der Logdateien.
|
||||
///
|
||||
/// Der Takt läuft über <see cref="DispatcherTimer"/> — das Avalonia-Gegenstück zum
|
||||
/// <c>System.Windows.Forms.Timer</c>, also auf dem Oberflächenfaden. Für eine Ansicht
|
||||
/// ist das genau richtig: Das Lesen selbst dauert Millisekunden, und so entfällt jedes
|
||||
/// Marshalling. Alles, was ohne Fenster laufen muss, liegt dagegen in
|
||||
/// <see cref="LogTail"/> und taktet dort über <c>PeriodicTimer</c>.
|
||||
/// </summary>
|
||||
public sealed partial class LogPageViewModel : PageViewModel
|
||||
{
|
||||
/// <summary>
|
||||
/// Obergrenze der angezeigten Zeilen.
|
||||
///
|
||||
/// Ohne Deckel wächst die Liste unbegrenzt — ein Agent, der eine Nacht durchläuft,
|
||||
/// hinterlässt sonst Hunderttausende Einträge im Speicher, und das Scrollen wird zäh.
|
||||
/// </summary>
|
||||
private const int MaxLines = 2000;
|
||||
|
||||
private readonly LogTail? _tail;
|
||||
private readonly DispatcherTimer? _timer;
|
||||
private readonly string _logDirectory;
|
||||
|
||||
public ObservableCollection<LogLine> Lines { get; } = [];
|
||||
|
||||
public ObservableCollection<string> Modules { get; } = ["Alle"];
|
||||
|
||||
public IReadOnlyList<string> Levels { get; } = ["Alle", "Info", "Warn", "Error"];
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedModule = "Alle";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _selectedLevel = "Alle";
|
||||
|
||||
/// <summary>
|
||||
/// Hängt die Ansicht am unteren Ende? Sobald jemand nach oben scrollt, soll nicht
|
||||
/// weitergesprungen werden — sonst kann man nichts lesen, während etwas läuft.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private bool _followTail = true;
|
||||
|
||||
public LogPageViewModel(AppHost? host) : base("Logs")
|
||||
{
|
||||
_logDirectory = host?.LogDirectory ?? "";
|
||||
|
||||
if (host is null) return; // Entwurfsmodus
|
||||
|
||||
_tail = new LogTail(_logDirectory);
|
||||
RefreshModules();
|
||||
|
||||
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(500) };
|
||||
_timer.Tick += (_, _) => Poll();
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void Poll()
|
||||
{
|
||||
if (_tail is null) return;
|
||||
|
||||
var module = SelectedModule == "Alle" ? null : SelectedModule;
|
||||
var level = SelectedLevel switch
|
||||
{
|
||||
"Info" => LogLevelFilter.Info,
|
||||
"Warn" => LogLevelFilter.Warn,
|
||||
"Error" => LogLevelFilter.Error,
|
||||
_ => LogLevelFilter.All
|
||||
};
|
||||
|
||||
foreach (var line in _tail.ReadNew(module, level))
|
||||
{
|
||||
Lines.Add(line);
|
||||
|
||||
// Von vorn kürzen statt am Ende zu deckeln: Das Neueste soll bleiben.
|
||||
while (Lines.Count > MaxLines)
|
||||
Lines.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Clear() => Lines.Clear();
|
||||
|
||||
[RelayCommand]
|
||||
private void RefreshModules()
|
||||
{
|
||||
if (_tail is null) return;
|
||||
|
||||
var current = SelectedModule;
|
||||
|
||||
Modules.Clear();
|
||||
Modules.Add("Alle");
|
||||
|
||||
foreach (var module in _tail.AvailableModules())
|
||||
Modules.Add(module);
|
||||
|
||||
// Auswahl halten, wenn es das Modul noch gibt.
|
||||
SelectedModule = Modules.Contains(current) ? current : "Alle";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenFolder() => SystemShell.OpenFolder(_logDirectory);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Das Gerüst des Hauptfensters: welche Bereiche es gibt und welcher gerade sichtbar ist.
|
||||
/// </summary>
|
||||
public sealed partial class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly AppHost? _host;
|
||||
|
||||
[ObservableProperty]
|
||||
private PageViewModel? _selectedPage;
|
||||
|
||||
public ObservableCollection<PageViewModel> Pages { get; }
|
||||
|
||||
/// <summary>Text der Statusleiste am unteren Rand.</summary>
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
public event Action? RequestExit;
|
||||
public event Action? RequestInstanceManagerDialog;
|
||||
|
||||
/// <summary>Für den Entwurfsmodus des Editors — ohne laufenden Aufbau.</summary>
|
||||
public MainWindowViewModel() : this(null) { }
|
||||
|
||||
public MainWindowViewModel(AppHost? host)
|
||||
{
|
||||
_host = host;
|
||||
|
||||
Pages =
|
||||
[
|
||||
new ChatPageViewModel(host),
|
||||
new LogPageViewModel(host),
|
||||
new AgentsPageViewModel(host),
|
||||
new SettingsPageViewModel(host),
|
||||
new TasksPageViewModel(host),
|
||||
new BackupPageViewModel(host),
|
||||
new InfoPageViewModel(host)
|
||||
];
|
||||
|
||||
SelectedPage = Pages[0];
|
||||
|
||||
StatusText = host is null
|
||||
? "Entwurfsmodus"
|
||||
: $"Instanz: {host.Instance.InstanceName} · "
|
||||
+ (host.Engine is null ? "Agenten deaktiviert (kein API-Key)" : "bereit");
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenInstanceFolder()
|
||||
{
|
||||
if (_host is null) return;
|
||||
SystemShell.OpenFolder(_host.InstancePath);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenLogFolder()
|
||||
{
|
||||
if (_host is null) return;
|
||||
SystemShell.OpenFolder(_host.LogDirectory);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void OpenInstanceManager()
|
||||
{
|
||||
RequestInstanceManagerDialog?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SelectPageByTitle(string title)
|
||||
{
|
||||
var target = Pages.FirstOrDefault(p => p.Title.Equals(title, StringComparison.OrdinalIgnoreCase));
|
||||
if (target is not null)
|
||||
SelectedPage = target;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Exit()
|
||||
{
|
||||
RequestExit?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ein Bereich des Hauptfensters.</summary>
|
||||
public abstract partial class PageViewModel(string title) : ViewModelBase
|
||||
{
|
||||
public string Title { get; } = title;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Platzhalter, solange der Bereich noch nicht portiert ist.
|
||||
/// </summary>
|
||||
public sealed partial class PlaceholderPageViewModel(string title, string note)
|
||||
: PageViewModel(title)
|
||||
{
|
||||
public string Note { get; } = note;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using ClawdDotNet.Core.Backup;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record RestoreResultData(string TargetDirectory, string? Passphrase, bool Overwrite);
|
||||
|
||||
public sealed partial class RestoreBackupViewModel : ViewModelBase
|
||||
{
|
||||
private readonly BackupManifest _manifest;
|
||||
|
||||
public string InstanceName => _manifest.InstanceName;
|
||||
public DateTime CreatedAt => _manifest.CreatedAt;
|
||||
public int FileCount => _manifest.Files.Count;
|
||||
public bool NeedsPassphrase => _manifest.HasSecrets;
|
||||
|
||||
public string SecretsDescription => _manifest.HasSecrets
|
||||
? $"{_manifest.SecretCount}, mit Passphrase geschützt"
|
||||
: _manifest.SecretCount > 0
|
||||
? $"{_manifest.SecretCount} entfernt"
|
||||
: "keine";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _targetDirectory = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _passphrase = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _overwrite;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusMessage = "";
|
||||
|
||||
public RestoreResultData? Result { get; private set; }
|
||||
|
||||
public event Action? CloseRequested;
|
||||
|
||||
public RestoreBackupViewModel() : this(new BackupManifest(), @"C:\Instance_Restored") { }
|
||||
|
||||
public RestoreBackupViewModel(BackupManifest manifest, string suggestedTarget)
|
||||
{
|
||||
_manifest = manifest;
|
||||
TargetDirectory = suggestedTarget;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(TargetDirectory))
|
||||
{
|
||||
StatusMessage = "Bitte einen Zielordner angeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (NeedsPassphrase && string.IsNullOrEmpty(Passphrase))
|
||||
{
|
||||
StatusMessage = "Bitte Passphrase für geschützte Zugangsdaten eingeben.";
|
||||
return;
|
||||
}
|
||||
|
||||
Result = new RestoreResultData(
|
||||
TargetDirectory.Trim(),
|
||||
NeedsPassphrase ? Passphrase : null,
|
||||
Overwrite);
|
||||
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result = null;
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Settings;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die Einstellungen-Seite (Anwendungs- und Instanz-Einstellungen).
|
||||
/// </summary>
|
||||
public sealed partial class SettingsPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly AppHost? _host;
|
||||
private readonly ILogger? _logger;
|
||||
|
||||
// ─── Allgemein & Pfade ───
|
||||
[ObservableProperty]
|
||||
private string _logDirectory = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _instancesDirectory = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _minimumLogLevel = "Info";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _maxLogLinesInUi = 2000;
|
||||
|
||||
[ObservableProperty]
|
||||
private int _logRefreshIntervalMs = 500;
|
||||
|
||||
// ─── API & OpenRouter ───
|
||||
[ObservableProperty]
|
||||
private string _openRouterBaseUrl = "https://openrouter.ai/api/v1/";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _statusCheckIntervalSeconds = 60;
|
||||
|
||||
// ─── Backup ───
|
||||
[ObservableProperty]
|
||||
private string _backupDirectory = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _autoBackupEnabled;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _autoBackupTime = "03:00";
|
||||
|
||||
[ObservableProperty]
|
||||
private int _backupKeepCount = 14;
|
||||
|
||||
// ─── Deploymentcenter ───
|
||||
[ObservableProperty]
|
||||
private string _deploymentcenterUrl = "https://dc.mhdf.de";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _deploymentcenterToken = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _deploymentcenterEnvironment = "production";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _errorReportingEnabled = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _updateCheckEnabled = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _updateChannel = "prod";
|
||||
|
||||
// ─── Lizenz ───
|
||||
[ObservableProperty]
|
||||
private string _licenseKey = "";
|
||||
|
||||
// ─── Instanz-Einstellungen ───
|
||||
[ObservableProperty]
|
||||
private string _instanceName = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _openRouterApiKey = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private decimal _instanceDailyCostUsd;
|
||||
|
||||
[ObservableProperty]
|
||||
private long _instanceDailyTokens;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
public List<string> LogLevels { get; } = ["Debug", "Info", "Warn", "Error"];
|
||||
public List<string> UpdateChannels { get; } = ["prod", "beta", "dev"];
|
||||
public List<string> Environments { get; } = ["production", "development", "staging"];
|
||||
|
||||
public SettingsPageViewModel() : this(null) { }
|
||||
|
||||
public SettingsPageViewModel(AppHost? host) : base("Einstellungen")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Settings");
|
||||
LoadFromHost();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void LoadFromHost()
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
var s = _host.Settings.AppSettings;
|
||||
LogDirectory = s.LogDirectory;
|
||||
InstancesDirectory = s.InstancesDirectory;
|
||||
MinimumLogLevel = s.MinimumLogLevel;
|
||||
MaxLogLinesInUi = s.MaxLogLinesInUi;
|
||||
LogRefreshIntervalMs = s.LogRefreshIntervalMs;
|
||||
OpenRouterBaseUrl = s.OpenRouterBaseUrl;
|
||||
StatusCheckIntervalSeconds = s.StatusCheckIntervalSeconds;
|
||||
BackupDirectory = s.BackupDirectory;
|
||||
AutoBackupEnabled = s.AutoBackupEnabled;
|
||||
AutoBackupTime = s.AutoBackupTime;
|
||||
BackupKeepCount = s.BackupKeepCount;
|
||||
DeploymentcenterUrl = s.DeploymentcenterUrl;
|
||||
DeploymentcenterToken = s.DeploymentcenterToken;
|
||||
DeploymentcenterEnvironment = s.DeploymentcenterEnvironment;
|
||||
ErrorReportingEnabled = s.ErrorReportingEnabled;
|
||||
UpdateCheckEnabled = s.UpdateCheckEnabled;
|
||||
UpdateChannel = s.UpdateChannel;
|
||||
LicenseKey = s.LicenseKey;
|
||||
|
||||
var inst = _host.Instance;
|
||||
InstanceName = inst.InstanceName;
|
||||
OpenRouterApiKey = inst.OpenRouterApiKey;
|
||||
InstanceDailyCostUsd = inst.Budget.DailyCostUsd;
|
||||
InstanceDailyTokens = inst.Budget.DailyTokens;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void SaveSettings()
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
var s = _host.Settings.AppSettings;
|
||||
s.LogDirectory = LogDirectory.Trim();
|
||||
s.InstancesDirectory = InstancesDirectory.Trim();
|
||||
s.MinimumLogLevel = MinimumLogLevel;
|
||||
s.MaxLogLinesInUi = MaxLogLinesInUi;
|
||||
s.LogRefreshIntervalMs = LogRefreshIntervalMs;
|
||||
s.OpenRouterBaseUrl = OpenRouterBaseUrl.Trim();
|
||||
s.StatusCheckIntervalSeconds = StatusCheckIntervalSeconds;
|
||||
s.BackupDirectory = BackupDirectory.Trim();
|
||||
s.AutoBackupEnabled = AutoBackupEnabled;
|
||||
s.AutoBackupTime = AutoBackupTime.Trim();
|
||||
s.BackupKeepCount = BackupKeepCount;
|
||||
s.DeploymentcenterUrl = DeploymentcenterUrl.Trim();
|
||||
s.DeploymentcenterToken = DeploymentcenterToken.Trim();
|
||||
s.DeploymentcenterEnvironment = DeploymentcenterEnvironment.Trim();
|
||||
s.ErrorReportingEnabled = ErrorReportingEnabled;
|
||||
s.UpdateCheckEnabled = UpdateCheckEnabled;
|
||||
s.UpdateChannel = UpdateChannel;
|
||||
s.LicenseKey = LicenseKey.Trim();
|
||||
|
||||
_host.Settings.Save();
|
||||
|
||||
var inst = _host.Instance;
|
||||
inst.InstanceName = InstanceName.Trim();
|
||||
inst.OpenRouterApiKey = OpenRouterApiKey.Trim();
|
||||
inst.Budget.DailyCostUsd = InstanceDailyCostUsd;
|
||||
inst.Budget.DailyTokens = InstanceDailyTokens;
|
||||
|
||||
_host.Directories.SaveInstanceConfig(_host.InstancePath, inst);
|
||||
|
||||
_logger?.LogInformation("Einstellungen und Instanz-Konfiguration gespeichert.");
|
||||
StatusText = "Einstellungen erfolgreich gespeichert.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Threading;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.App.Models;
|
||||
using ClawdDotNet.App.Services;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClawdDotNet.Core.Scheduling;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed record JobDisplayItemViewModel(
|
||||
string JobType,
|
||||
string JobId,
|
||||
string AgentId,
|
||||
string AgentName,
|
||||
string ToolName,
|
||||
string CronExpression,
|
||||
string TaskMessage,
|
||||
bool RunOnStart,
|
||||
string NextRun,
|
||||
string LastRun,
|
||||
string LastStatus,
|
||||
string Status
|
||||
);
|
||||
|
||||
public sealed record ServiceDisplayItemViewModel(
|
||||
string ServiceId,
|
||||
string Name,
|
||||
string Type,
|
||||
int Port,
|
||||
string Status,
|
||||
string Description
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die Aufgaben-Seite (Jobs, Dienste, Verlauf).
|
||||
/// </summary>
|
||||
public sealed partial class TasksPageViewModel : PageViewModel
|
||||
{
|
||||
private readonly AppHost? _host;
|
||||
private readonly JobHistoryService? _jobHistoryService;
|
||||
private readonly ILogger? _logger;
|
||||
private readonly Dictionary<string, DateTime> _jobLastRunTimes = new();
|
||||
|
||||
public ObservableCollection<JobDisplayItemViewModel> JobEntries { get; } = [];
|
||||
public ObservableCollection<ServiceDisplayItemViewModel> ServiceEntries { get; } = [];
|
||||
public ObservableCollection<JobHistoryEntry> JobHistoryEntries { get; } = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private JobDisplayItemViewModel? _selectedJob;
|
||||
|
||||
[ObservableProperty]
|
||||
private ServiceDisplayItemViewModel? _selectedService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRunningJob;
|
||||
|
||||
public event Func<AddJobViewModel, Task<AddJobResultData?>>? RequestAddJobDialog;
|
||||
public event Func<AddServiceViewModel, Task<AddServiceResultData?>>? RequestAddServiceDialog;
|
||||
|
||||
public TasksPageViewModel() : this(null) { }
|
||||
|
||||
public TasksPageViewModel(AppHost? host) : base("Aufgaben")
|
||||
{
|
||||
_host = host;
|
||||
|
||||
if (host is not null)
|
||||
{
|
||||
_jobHistoryService = new JobHistoryService(host.InstancePath);
|
||||
_logger = host.LoggerFactory.CreateLogger("ClawdDotNet.Desktop.Tasks");
|
||||
|
||||
EnsureBuiltInServices();
|
||||
RefreshJobList();
|
||||
RefreshServiceList();
|
||||
RefreshJobHistory();
|
||||
}
|
||||
else
|
||||
{
|
||||
StatusText = "Entwurfsmodus";
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveInstanceConfig()
|
||||
{
|
||||
if (_host is null) return;
|
||||
_host.Directories.SaveInstanceConfig(_host.InstancePath, _host.Instance);
|
||||
}
|
||||
|
||||
private void EnsureBuiltInServices()
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
var defaults = BuiltInServices.CreateDefaults();
|
||||
var changed = false;
|
||||
|
||||
foreach (var def in defaults)
|
||||
{
|
||||
if (_host.Instance.Services.Any(s => s.ServiceId == def.ServiceId))
|
||||
continue;
|
||||
|
||||
_host.Instance.Services.Insert(0, def);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
SaveInstanceConfig();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RefreshJobList()
|
||||
{
|
||||
JobEntries.Clear();
|
||||
if (_host is null) return;
|
||||
|
||||
foreach (var agent in _host.Instance.Agents)
|
||||
{
|
||||
// Agent Wakeup Jobs
|
||||
if (agent.Scheduler is not null)
|
||||
{
|
||||
var nextRun = CalculateNextRun(agent.Scheduler.Cron);
|
||||
var agentLastRun = _jobLastRunTimes.TryGetValue($"agent_{agent.AgentId}", out var agentLastTime)
|
||||
? agentLastTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
: "—";
|
||||
|
||||
JobEntries.Add(new JobDisplayItemViewModel(
|
||||
"Agent Wakeup",
|
||||
$"agent_{agent.AgentId}",
|
||||
agent.AgentId,
|
||||
agent.DisplayName,
|
||||
"—",
|
||||
agent.Scheduler.Cron,
|
||||
agent.Scheduler.TaskMessage,
|
||||
agent.Scheduler.RunOnStart,
|
||||
nextRun,
|
||||
agentLastRun,
|
||||
"—",
|
||||
_host.Scanner is not null ? "Aktiv" : "Inaktiv"
|
||||
));
|
||||
}
|
||||
|
||||
// Tool Jobs
|
||||
foreach (var toolJob in agent.ToolJobs)
|
||||
{
|
||||
var nextRun = CalculateNextRun(toolJob.Cron);
|
||||
var toolLastRun = _jobLastRunTimes.TryGetValue(toolJob.JobId, out var lastTime)
|
||||
? lastTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
: "—";
|
||||
|
||||
JobEntries.Add(new JobDisplayItemViewModel(
|
||||
"Tool Job",
|
||||
toolJob.JobId,
|
||||
agent.AgentId,
|
||||
agent.DisplayName,
|
||||
toolJob.ToolName,
|
||||
toolJob.Cron,
|
||||
toolJob.JobTypeId,
|
||||
toolJob.RunOnStart,
|
||||
nextRun,
|
||||
toolLastRun,
|
||||
"—",
|
||||
toolJob.Enabled ? "Aktiv" : "Deaktiviert"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RefreshServiceList()
|
||||
{
|
||||
ServiceEntries.Clear();
|
||||
if (_host is null) return;
|
||||
|
||||
foreach (var svc in _host.Instance.Services)
|
||||
{
|
||||
ServiceEntries.Add(new ServiceDisplayItemViewModel(
|
||||
svc.ServiceId,
|
||||
svc.Name,
|
||||
svc.Type,
|
||||
svc.Port,
|
||||
svc.Enabled ? "Bereit" : "Deaktiviert",
|
||||
svc.Description
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RefreshJobHistory()
|
||||
{
|
||||
JobHistoryEntries.Clear();
|
||||
if (_jobHistoryService is null) return;
|
||||
|
||||
foreach (var entry in _jobHistoryService.GetAll().Take(200))
|
||||
JobHistoryEntries.Add(entry);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddJobAsync()
|
||||
{
|
||||
if (_host is null || RequestAddJobDialog is null) return;
|
||||
|
||||
var vm = new AddJobViewModel(_host.Instance.Agents);
|
||||
var result = await RequestAddJobDialog(vm);
|
||||
if (result is null) return;
|
||||
|
||||
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == result.AgentId);
|
||||
if (agent is null) return;
|
||||
|
||||
if (result.IsToolJob)
|
||||
{
|
||||
agent.ToolJobs.Add(new ToolJobConfig
|
||||
{
|
||||
ToolName = result.ToolName,
|
||||
JobTypeId = result.JobTypeId,
|
||||
Cron = result.CronExpression,
|
||||
RunOnStart = result.RunOnStart,
|
||||
Enabled = true
|
||||
});
|
||||
|
||||
_logger?.LogInformation("Tool-Job hinzugefügt: Agent={Agent}, Tool={Tool}, Cron={Cron}",
|
||||
agent.DisplayName, result.ToolName, result.CronExpression);
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.Scheduler = new SchedulerConfig
|
||||
{
|
||||
Cron = result.CronExpression,
|
||||
RunOnStart = result.RunOnStart,
|
||||
TaskMessage = result.TaskMessage
|
||||
};
|
||||
|
||||
_logger?.LogInformation("Agent-Wakeup-Job hinzugefügt: Agent={Agent}, Cron={Cron}",
|
||||
agent.DisplayName, result.CronExpression);
|
||||
}
|
||||
|
||||
SaveInstanceConfig();
|
||||
RefreshJobList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task EditJobAsync()
|
||||
{
|
||||
if (_host is null || SelectedJob is null || RequestAddJobDialog is null) return;
|
||||
|
||||
var entry = SelectedJob;
|
||||
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
|
||||
if (agent is null) return;
|
||||
|
||||
var isToolJob = entry.JobType == "Tool Job";
|
||||
var vm = new AddJobViewModel(_host.Instance.Agents)
|
||||
{
|
||||
SelectedAgent = agent,
|
||||
IsToolJob = isToolJob,
|
||||
SelectedToolName = isToolJob ? entry.ToolName : "FileRW",
|
||||
JobTypeId = isToolJob ? entry.TaskMessage : "PollJob",
|
||||
CronExpression = entry.CronExpression,
|
||||
TaskMessage = isToolJob ? "" : entry.TaskMessage,
|
||||
RunOnStart = entry.RunOnStart
|
||||
};
|
||||
|
||||
var result = await RequestAddJobDialog(vm);
|
||||
if (result is null) return;
|
||||
|
||||
if (isToolJob)
|
||||
{
|
||||
var toolJob = agent.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId);
|
||||
if (toolJob is not null)
|
||||
{
|
||||
toolJob.Cron = result.CronExpression;
|
||||
toolJob.RunOnStart = result.RunOnStart;
|
||||
toolJob.JobTypeId = result.JobTypeId;
|
||||
toolJob.ToolName = result.ToolName;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (agent.Scheduler is not null)
|
||||
{
|
||||
agent.Scheduler.Cron = result.CronExpression;
|
||||
agent.Scheduler.TaskMessage = result.TaskMessage;
|
||||
agent.Scheduler.RunOnStart = result.RunOnStart;
|
||||
}
|
||||
}
|
||||
|
||||
SaveInstanceConfig();
|
||||
RefreshJobList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveJob()
|
||||
{
|
||||
if (_host is null || SelectedJob is null) return;
|
||||
|
||||
var entry = SelectedJob;
|
||||
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
|
||||
if (agent is null) return;
|
||||
|
||||
if (entry.JobType == "Tool Job")
|
||||
{
|
||||
agent.ToolJobs.RemoveAll(j => j.JobId == entry.JobId);
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.Scheduler = null;
|
||||
}
|
||||
|
||||
SaveInstanceConfig();
|
||||
RefreshJobList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task RunJobNowAsync()
|
||||
{
|
||||
if (_host is null || SelectedJob is null) return;
|
||||
|
||||
var entry = SelectedJob;
|
||||
var agent = _host.Instance.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId);
|
||||
if (agent is null) return;
|
||||
|
||||
IsRunningJob = true;
|
||||
StatusText = $"Führe Job für '{agent.DisplayName}' aus…";
|
||||
|
||||
try
|
||||
{
|
||||
if (entry.JobType == "Tool Job")
|
||||
{
|
||||
if (_host.Scanner is null)
|
||||
{
|
||||
StatusText = "Scanner ist nicht aktiv.";
|
||||
return;
|
||||
}
|
||||
|
||||
var jobConfig = agent.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId);
|
||||
if (jobConfig is null) return;
|
||||
|
||||
var ran = await _host.Scanner.RunTaskNowAsync(
|
||||
$"tj-{agent.AgentId}-{jobConfig.JobId}", CancellationToken.None);
|
||||
|
||||
_jobLastRunTimes[jobConfig.JobId] = DateTime.Now;
|
||||
var info = ran ? "Ausgeführt" : "Nicht bereit";
|
||||
|
||||
_jobHistoryService?.Add(new JobHistoryEntry
|
||||
{
|
||||
JobName = jobConfig.JobTypeId,
|
||||
Agent = agent.DisplayName,
|
||||
Time = DateTime.Now,
|
||||
JobDescription = $"Tool Job: {jobConfig.ToolName}",
|
||||
Info = info,
|
||||
Status = "Manual"
|
||||
});
|
||||
|
||||
StatusText = $"Tool Job '{jobConfig.JobTypeId}' abgeschlossen ({info}).";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_host.Engine is null)
|
||||
{
|
||||
StatusText = "Agent-Engine ist nicht aktiv (kein API-Key).";
|
||||
return;
|
||||
}
|
||||
|
||||
var taskMessage = agent.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus.";
|
||||
var result = await _host.Engine.RunAsync(agent, taskMessage, _host.Instance.InstanceId, CancellationToken.None);
|
||||
|
||||
_jobLastRunTimes[$"agent_{agent.AgentId}"] = DateTime.Now;
|
||||
|
||||
_jobHistoryService?.Add(new JobHistoryEntry
|
||||
{
|
||||
JobName = "Agent Wakeup",
|
||||
Agent = agent.DisplayName,
|
||||
Time = DateTime.Now,
|
||||
JobDescription = taskMessage,
|
||||
Info = $"{result.Status} ({result.StepCount} Steps, {result.TokensUsed:N0} Tokens)",
|
||||
Status = "Manual"
|
||||
});
|
||||
|
||||
StatusText = $"Agent '{agent.DisplayName}' abgeschlossen: {result.Status} ({result.TokensUsed:N0} Tokens).";
|
||||
}
|
||||
|
||||
RefreshJobList();
|
||||
RefreshJobHistory();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Fehler beim Ausführen: {ex.Message}";
|
||||
_logger?.LogError(ex, "Job-Ausführung fehlgeschlagen");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRunningJob = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddServiceAsync()
|
||||
{
|
||||
if (_host is null || RequestAddServiceDialog is null) return;
|
||||
|
||||
var vm = new AddServiceViewModel();
|
||||
var result = await RequestAddServiceDialog(vm);
|
||||
if (result is null) return;
|
||||
|
||||
var svcConfig = new ServiceConfig
|
||||
{
|
||||
Name = result.ServiceName,
|
||||
Type = result.ServiceType,
|
||||
Port = result.ServicePort,
|
||||
Enabled = true,
|
||||
Description = result.ServiceDescription
|
||||
};
|
||||
|
||||
_host.Instance.Services.Add(svcConfig);
|
||||
SaveInstanceConfig();
|
||||
RefreshServiceList();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void RemoveService()
|
||||
{
|
||||
if (_host is null || SelectedService is null) return;
|
||||
|
||||
_host.Instance.Services.RemoveAll(s => s.ServiceId == SelectedService.ServiceId);
|
||||
SaveInstanceConfig();
|
||||
RefreshServiceList();
|
||||
}
|
||||
|
||||
private static string CalculateNextRun(string? cronExpr)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cronExpr)) return "—";
|
||||
try
|
||||
{
|
||||
var cron = CronExpression.Parse(cronExpr);
|
||||
var next = cron.GetNextOccurrence(DateTime.UtcNow);
|
||||
return next?.ToLocalTime().ToString("yyyy-MM-dd HH:mm") ?? "—";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "Ungültig";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
using ClawdDotNet.Models;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
public sealed partial class ToolSettingsViewModel : ViewModelBase
|
||||
{
|
||||
public string ToolName { get; }
|
||||
public object? SettingsObject { get; }
|
||||
|
||||
// FTP
|
||||
[ObservableProperty] private string _ftpHost = "";
|
||||
[ObservableProperty] private int _ftpPort = 21;
|
||||
[ObservableProperty] private string _ftpUsername = "";
|
||||
[ObservableProperty] private string _ftpPassword = "";
|
||||
[ObservableProperty] private string _ftpRootPath = "./data/";
|
||||
|
||||
// Mail
|
||||
[ObservableProperty] private string _mailUsername = "";
|
||||
[ObservableProperty] private string _mailPassword = "";
|
||||
[ObservableProperty] private string _mailImapHost = "";
|
||||
[ObservableProperty] private int _mailImapPort = 993;
|
||||
[ObservableProperty] private string _mailSmtpHost = "";
|
||||
[ObservableProperty] private int _mailSmtpPort = 587;
|
||||
[ObservableProperty] private string _mailAllowedRecipients = "";
|
||||
|
||||
// Database
|
||||
[ObservableProperty] private DatabaseType _dbType = DatabaseType.MySql;
|
||||
[ObservableProperty] private string _dbConnectionString = "";
|
||||
[ObservableProperty] private DatabaseAccessLevel _dbAccessLevel = DatabaseAccessLevel.ReadOnly;
|
||||
[ObservableProperty] private string _dbAllowedTables = "";
|
||||
|
||||
// Telegram
|
||||
[ObservableProperty] private string _telegramBotToken = "";
|
||||
[ObservableProperty] private string _telegramDefaultChatId = "";
|
||||
[ObservableProperty] private string _telegramAllowedChatIds = "";
|
||||
|
||||
// DirectAPI
|
||||
[ObservableProperty] private string _directApiDefaultProvider = "twelvedata";
|
||||
[ObservableProperty] private int _directApiCacheTtlSeconds = 60;
|
||||
[ObservableProperty] private string _directApiTwelveDataKey = "";
|
||||
[ObservableProperty] private string _directApiAlphaVantageKey = "";
|
||||
|
||||
// WebFetch
|
||||
[ObservableProperty] private string _webFetchAllowedDomains = "";
|
||||
[ObservableProperty] private int _webFetchMaxResponseKb = 512;
|
||||
[ObservableProperty] private string _webFetchUserAgent = "ClawdDotNet-Agent/1.0";
|
||||
|
||||
// SocialMediaManager
|
||||
[ObservableProperty] private string _smmXApiKey = "";
|
||||
[ObservableProperty] private string _smmXWatchAccounts = "";
|
||||
[ObservableProperty] private string _smmRedditWatchSubreddits = "";
|
||||
[ObservableProperty] private int _smmRedditPostLimit = 15;
|
||||
[ObservableProperty] private string _smmOpenRouterApiKey = "";
|
||||
[ObservableProperty] private string _smmSttModel = "openai/whisper-1";
|
||||
[ObservableProperty] private string _smmYoutubeChannels = "";
|
||||
|
||||
// FileRW
|
||||
[ObservableProperty] private string _fileRwPersonalAllowedExtensions = ".txt,.json,.md,.html,.js,.css";
|
||||
[ObservableProperty] private FileRWAccessLevel _fileRwSharedAccessLevel = FileRWAccessLevel.Denied;
|
||||
[ObservableProperty] private string _fileRwSharedAllowedExtensions = ".txt,.json,.md";
|
||||
[ObservableProperty] private string _fileRwProtectedPaths = "stocks/";
|
||||
|
||||
public List<DatabaseType> DbTypes { get; } = [DatabaseType.MySql, DatabaseType.Postgres, DatabaseType.MsSql, DatabaseType.MongoDb];
|
||||
public List<DatabaseAccessLevel> DbAccessLevels { get; } = [DatabaseAccessLevel.ReadOnly, DatabaseAccessLevel.ReadWrite, DatabaseAccessLevel.Admin];
|
||||
public List<FileRWAccessLevel> FileRwAccessLevels { get; } = [FileRWAccessLevel.Denied, FileRWAccessLevel.Read, FileRWAccessLevel.ReadWrite, FileRWAccessLevel.Admin];
|
||||
|
||||
public Dictionary<string, object?>? Result { get; private set; }
|
||||
public event Action? CloseRequested;
|
||||
|
||||
public ToolSettingsViewModel() : this("FTP", new()) { }
|
||||
|
||||
public ToolSettingsViewModel(string toolName, Dictionary<string, object?> config)
|
||||
{
|
||||
ToolName = toolName;
|
||||
SettingsObject = ToolSettingsFactory.CreateViewModel(toolName, config);
|
||||
|
||||
switch (SettingsObject)
|
||||
{
|
||||
case FTPToolSettings ftp:
|
||||
FtpHost = ftp.Host;
|
||||
FtpPort = ftp.Port;
|
||||
FtpUsername = ftp.Username;
|
||||
FtpPassword = ftp.Password;
|
||||
FtpRootPath = ftp.RootPath;
|
||||
break;
|
||||
|
||||
case MailToolSettings mail:
|
||||
MailUsername = mail.Username;
|
||||
MailPassword = mail.Password;
|
||||
MailImapHost = mail.ImapHost;
|
||||
MailImapPort = mail.ImapPort;
|
||||
MailSmtpHost = mail.SmtpHost;
|
||||
MailSmtpPort = mail.SmtpPort;
|
||||
MailAllowedRecipients = mail.AllowedRecipients;
|
||||
break;
|
||||
|
||||
case DatabaseToolSettings db:
|
||||
DbType = db.Type;
|
||||
DbConnectionString = db.ConnectionString;
|
||||
DbAccessLevel = db.AccessLevel;
|
||||
DbAllowedTables = db.AllowedTables;
|
||||
break;
|
||||
|
||||
case TelegramToolSettings tg:
|
||||
TelegramBotToken = tg.BotToken;
|
||||
TelegramDefaultChatId = tg.DefaultChatId;
|
||||
TelegramAllowedChatIds = tg.AllowedChatIds;
|
||||
break;
|
||||
|
||||
case DirectAPIToolSettings dapi:
|
||||
DirectApiDefaultProvider = dapi.DefaultProvider;
|
||||
DirectApiCacheTtlSeconds = dapi.CacheTtlSeconds;
|
||||
DirectApiTwelveDataKey = dapi.TwelveDataKey;
|
||||
DirectApiAlphaVantageKey = dapi.AlphaVantageKey;
|
||||
break;
|
||||
|
||||
case WebFetchToolSettings wf:
|
||||
WebFetchAllowedDomains = wf.AllowedDomains;
|
||||
WebFetchMaxResponseKb = wf.MaxResponseKb;
|
||||
WebFetchUserAgent = wf.UserAgent;
|
||||
break;
|
||||
|
||||
case SocialMediaManagerToolSettings smm:
|
||||
SmmXApiKey = smm.XApiKey;
|
||||
SmmXWatchAccounts = smm.XWatchAccounts;
|
||||
SmmRedditWatchSubreddits = smm.RedditWatchSubreddits;
|
||||
SmmRedditPostLimit = smm.RedditPostLimit;
|
||||
SmmOpenRouterApiKey = smm.OpenRouterApiKey;
|
||||
SmmSttModel = smm.STTModel;
|
||||
SmmYoutubeChannels = smm.YoutubeChannels;
|
||||
break;
|
||||
|
||||
case FileRWToolSettings frw:
|
||||
FileRwPersonalAllowedExtensions = frw.PersonalAllowedExtensions;
|
||||
FileRwSharedAccessLevel = frw.SharedAccessLevel;
|
||||
FileRwSharedAllowedExtensions = frw.SharedAllowedExtensions;
|
||||
FileRwProtectedPaths = frw.ProtectedPaths;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Confirm()
|
||||
{
|
||||
switch (SettingsObject)
|
||||
{
|
||||
case FTPToolSettings ftp:
|
||||
ftp.Host = FtpHost;
|
||||
ftp.Port = FtpPort;
|
||||
ftp.Username = FtpUsername;
|
||||
ftp.Password = FtpPassword;
|
||||
ftp.RootPath = FtpRootPath;
|
||||
break;
|
||||
|
||||
case MailToolSettings mail:
|
||||
mail.Username = MailUsername;
|
||||
mail.Password = MailPassword;
|
||||
mail.ImapHost = MailImapHost;
|
||||
mail.ImapPort = MailImapPort;
|
||||
mail.SmtpHost = MailSmtpHost;
|
||||
mail.SmtpPort = MailSmtpPort;
|
||||
mail.AllowedRecipients = MailAllowedRecipients;
|
||||
break;
|
||||
|
||||
case DatabaseToolSettings db:
|
||||
db.Type = DbType;
|
||||
db.ConnectionString = DbConnectionString;
|
||||
db.AccessLevel = DbAccessLevel;
|
||||
db.AllowedTables = DbAllowedTables;
|
||||
break;
|
||||
|
||||
case TelegramToolSettings tg:
|
||||
tg.BotToken = TelegramBotToken;
|
||||
tg.DefaultChatId = TelegramDefaultChatId;
|
||||
tg.AllowedChatIds = TelegramAllowedChatIds;
|
||||
break;
|
||||
|
||||
case DirectAPIToolSettings dapi:
|
||||
dapi.DefaultProvider = DirectApiDefaultProvider;
|
||||
dapi.CacheTtlSeconds = DirectApiCacheTtlSeconds;
|
||||
dapi.TwelveDataKey = DirectApiTwelveDataKey;
|
||||
dapi.AlphaVantageKey = DirectApiAlphaVantageKey;
|
||||
break;
|
||||
|
||||
case WebFetchToolSettings wf:
|
||||
wf.AllowedDomains = WebFetchAllowedDomains;
|
||||
wf.MaxResponseKb = WebFetchMaxResponseKb;
|
||||
wf.UserAgent = WebFetchUserAgent;
|
||||
break;
|
||||
|
||||
case SocialMediaManagerToolSettings smm:
|
||||
smm.XApiKey = SmmXApiKey;
|
||||
smm.XWatchAccounts = SmmXWatchAccounts;
|
||||
smm.RedditWatchSubreddits = SmmRedditWatchSubreddits;
|
||||
smm.RedditPostLimit = SmmRedditPostLimit;
|
||||
smm.OpenRouterApiKey = SmmOpenRouterApiKey;
|
||||
smm.STTModel = SmmSttModel;
|
||||
smm.YoutubeChannels = SmmYoutubeChannels;
|
||||
break;
|
||||
|
||||
case FileRWToolSettings frw:
|
||||
frw.PersonalAllowedExtensions = FileRwPersonalAllowedExtensions;
|
||||
frw.SharedAccessLevel = FileRwSharedAccessLevel;
|
||||
frw.SharedAllowedExtensions = FileRwSharedAllowedExtensions;
|
||||
frw.ProtectedPaths = FileRwProtectedPaths;
|
||||
break;
|
||||
}
|
||||
|
||||
Result = ToolSettingsFactory.ToConfig(ToolName, SettingsObject) ?? new();
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Cancel()
|
||||
{
|
||||
Result = null;
|
||||
CloseRequested?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// Grundlage aller Ansichtsmodelle.
|
||||
///
|
||||
/// <see cref="ObservableObject"/> aus dem CommunityToolkit bringt
|
||||
/// <c>INotifyPropertyChanged</c> samt Quelltextgenerator mit — aus
|
||||
/// <c>[ObservableProperty] private string _name;</c> wird die vollständige Eigenschaft
|
||||
/// mit Benachrichtigung. Das spart gegenüber der WinForms-Fassung, in der die
|
||||
/// Oberfläche bei jeder Änderung von Hand nachgezogen wurde
|
||||
/// (38 Stellen mit <c>Invoke</c>/<c>BeginInvoke</c>).
|
||||
/// </summary>
|
||||
public abstract class ViewModelBase : ObservableObject;
|
||||
@@ -0,0 +1,51 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="380"
|
||||
Width="480" Height="400"
|
||||
Padding="16"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
Title="Neuen Agenten anlegen"
|
||||
x:Class="ClawdDotNet.Desktop.Views.AddAgentWindow"
|
||||
x:DataType="vm:AddAgentViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,16,0,0">
|
||||
<Button Content="Abbrechen" Command="{Binding CancelCommand}" />
|
||||
<Button Content="Anlegen" Command="{Binding ConfirmCommand}" Classes="accent" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" Foreground="Red" Margin="0,8,0,0" />
|
||||
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Agent-ID (eindeutiger Schlüssel):" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding AgentId}" PlaceholderText="z.B. dev-assistant" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Anzeigename:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding DisplayName}" PlaceholderText="z.B. Dev Assistant" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Standard-Modell:" FontWeight="Bold" />
|
||||
<ComboBox ItemsSource="{Binding AvailableModels}" SelectedItem="{Binding SelectedModel}" HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Beschreibung / Rolle:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding Description}" PlaceholderText="Kurze Beschreibung der Rolle..." AcceptsReturn="True" Height="60" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class AddAgentWindow : Window
|
||||
{
|
||||
public AddAgentWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is AddAgentViewModel vm)
|
||||
{
|
||||
vm.CloseRequested += () => Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
|
||||
Width="520" Height="460"
|
||||
Padding="16"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
Title="Job hinzufügen / bearbeiten"
|
||||
x:Class="ClawdDotNet.Desktop.Views.AddJobWindow"
|
||||
x:DataType="vm:AddJobViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,16,0,0">
|
||||
<Button Content="Abbrechen" Command="{Binding CancelCommand}" />
|
||||
<Button Content="Speichern" Command="{Binding ConfirmCommand}" Classes="accent" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" Foreground="Red" Margin="0,8,0,0" />
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<!-- Agent Selection -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Agent:" FontWeight="Bold" />
|
||||
<ComboBox ItemsSource="{Binding AvailableAgents}" SelectedItem="{Binding SelectedAgent}" HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Job Type RadioButtons -->
|
||||
<StackPanel Spacing="4" Margin="0,4,0,0">
|
||||
<TextBlock Text="Job-Typ:" FontWeight="Bold" />
|
||||
<RadioButton Content="Agent Wakeup (Periodische Ausführung)" IsChecked="{Binding IsAgentWakeup}" />
|
||||
<RadioButton Content="Tool Job (Werkzeug-spezifischer Job)" IsChecked="{Binding IsToolJob}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Tool Job Details -->
|
||||
<StackPanel Spacing="8" IsVisible="{Binding IsToolJob}" Margin="12,4,0,0">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Tool:" FontWeight="Bold" />
|
||||
<ComboBox ItemsSource="{Binding AvailableTools}" SelectedItem="{Binding SelectedToolName}" HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Job-Typ-ID / Name:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding JobTypeId}" PlaceholderText="z.B. PollMail" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Agent Wakeup Details -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding IsAgentWakeup}">
|
||||
<TextBlock Text="Aufgabenbeschreibung / Nachricht:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding TaskMessage}" PlaceholderText="Nachricht an den Agenten..." AcceptsReturn="True" Height="60" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Cron & Options -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Cron-Ausdruck:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding CronExpression}" PlaceholderText="0 * * * *" />
|
||||
<TextBlock Text="Syntax: Min Std Tag Mon Wochentag (z.B. '*/15 * * * *' alle 15 Minuten)" Classes="caption" />
|
||||
</StackPanel>
|
||||
|
||||
<CheckBox Content="Beim Anwendungsstart einmalig sofort ausführen" IsChecked="{Binding RunOnStart}" Margin="0,4,0,0" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class AddJobWindow : Window
|
||||
{
|
||||
public AddJobWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is AddJobViewModel vm)
|
||||
{
|
||||
vm.CloseRequested += () => Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="450" d:DesignHeight="350"
|
||||
Width="480" Height="360"
|
||||
Padding="16"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
Title="Dienst (Service) hinzufügen"
|
||||
x:Class="ClawdDotNet.Desktop.Views.AddServiceWindow"
|
||||
x:DataType="vm:AddServiceViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,16,0,0">
|
||||
<Button Content="Abbrechen" Command="{Binding CancelCommand}" />
|
||||
<Button Content="Hinzufügen" Command="{Binding ConfirmCommand}" Classes="accent" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" Foreground="Red" Margin="0,8,0,0" />
|
||||
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Name des Dienstes:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding ServiceName}" PlaceholderText="z.B. WebHookService" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4" Margin="0,0,4,0">
|
||||
<TextBlock Text="Typ:" FontWeight="Bold" />
|
||||
<ComboBox ItemsSource="{Binding AvailableServiceTypes}" SelectedItem="{Binding ServiceType}" HorizontalAlignment="Stretch" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="4" Margin="4,0,0,0">
|
||||
<TextBlock Text="Port:" FontWeight="Bold" />
|
||||
<NumericUpDown Value="{Binding ServicePort}" Minimum="1" Maximum="65535" FormatString="0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Beschreibung:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding ServiceDescription}" PlaceholderText="Optionale Beschreibung..." AcceptsReturn="True" Height="60" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class AddServiceWindow : Window
|
||||
{
|
||||
public AddServiceWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is AddServiceViewModel vm)
|
||||
{
|
||||
vm.CloseRequested += () => Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
|
||||
Padding="16"
|
||||
x:Class="ClawdDotNet.Desktop.Views.AgentsPageView"
|
||||
x:DataType="vm:AgentsPageViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Statusbar am unteren Rand -->
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<!-- Master-Detail Split -->
|
||||
<Grid ColumnDefinitions="260,*">
|
||||
|
||||
<!-- Linke Spalte: Agenten-Liste -->
|
||||
<DockPanel Grid.Column="0" Margin="0,0,12,0">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
<Button Content="Neu" Command="{Binding AddAgentCommand}" Classes="accent" />
|
||||
<Button Content="Entfernen" Command="{Binding RemoveAgentCommand}" IsEnabled="{Binding SelectedAgent, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<Button Content="Speichern" Command="{Binding SaveAgentsCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Liste -->
|
||||
<ListBox ItemsSource="{Binding Agents}" SelectedItem="{Binding SelectedAgent}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="vm:AgentItemViewModel">
|
||||
<StackPanel Margin="4">
|
||||
<TextBlock Text="{Binding DisplayName}" FontWeight="Bold" />
|
||||
<TextBlock Text="{Binding Model}" Classes="caption" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
<!-- Rechte Spalte: Agenten-Details -->
|
||||
<ContentControl Grid.Column="1" Content="{Binding SelectedAgent}">
|
||||
<ContentControl.ContentTemplate>
|
||||
<DataTemplate DataType="vm:AgentItemViewModel">
|
||||
<TabControl>
|
||||
|
||||
<!-- TAB 1: Allgemein & Modell -->
|
||||
<TabItem Header="Allgemein & Modell">
|
||||
<ScrollViewer Padding="8">
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Agent ID:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding AgentId}" IsReadOnly="True" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Anzeigename:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding DisplayName}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Modell:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding $parent[UserControl].((vm:AgentsPageViewModel)DataContext).KnownModels}" SelectedItem="{Binding Model}" HorizontalAlignment="Stretch" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Rolle / Beschreibung:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding Description}" AcceptsReturn="True" Height="60" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Prompt Caching:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="4" Grid.Column="1" ItemsSource="{Binding $parent[UserControl].((vm:AgentsPageViewModel)DataContext).CachingOptions}" SelectedItem="{Binding PromptCaching}" Width="150" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- Limits Card -->
|
||||
<Border Classes="card" Margin="0,8,0,0">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Schutzgrenzen (LoopGuard & Budget)" Classes="heading" />
|
||||
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto">
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="Max. Schritte pro Run:" />
|
||||
<NumericUpDown Value="{Binding MaxSteps}" Minimum="1" Maximum="100" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Spacing="4" Margin="8,0,0,0">
|
||||
<TextBlock Text="Timeout (Sekunden):" />
|
||||
<NumericUpDown Value="{Binding TimeoutSeconds}" Minimum="10" Maximum="3600" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="4" Margin="0,8,0,0">
|
||||
<TextBlock Text="Max. Kontext-Tokens:" />
|
||||
<NumericUpDown Value="{Binding MaxContextTokens}" Minimum="1000" Maximum="2000000" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="1" Grid.Column="1" Spacing="4" Margin="8,8,0,0">
|
||||
<TextBlock Text="Max. Tool-Ergebnis (Zeichen):" />
|
||||
<NumericUpDown Value="{Binding MaxToolResultChars}" Minimum="1000" Maximum="500000" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- TAB 2: System Prompt & Seele -->
|
||||
<TabItem Header="System-Prompt & Soul">
|
||||
<ScrollViewer Padding="8">
|
||||
<StackPanel Spacing="12">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="System-Prompt (Zusätzliche Anweisungen):" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding SystemPrompt}" AcceptsReturn="True" Height="120" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Identität (Identity):" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding Identity}" AcceptsReturn="True" Height="100" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Seele & Werte (Soul):" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding Soul}" AcceptsReturn="True" Height="100" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<!-- TAB 3: Werkzeuge (Tools) -->
|
||||
<TabItem Header="Werkzeuge (Tools)">
|
||||
<ScrollViewer Padding="8">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Zugelassene Werkzeuge für diesen Agenten:" Classes="heading" />
|
||||
<ItemsControl ItemsSource="{Binding AvailableTools}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:ToolItemViewModel">
|
||||
<Grid ColumnDefinitions="200,Auto" Margin="0,4">
|
||||
<CheckBox Content="{Binding Name}" IsChecked="{Binding IsEnabled}" Grid.Column="0" VerticalAlignment="Center" />
|
||||
<Button Content="⚙ Konfigurieren" Command="{Binding $parent[UserControl].((vm:AgentsPageViewModel)DataContext).ConfigureToolCommand}" CommandParameter="{Binding}" IsEnabled="{Binding IsEnabled}" Grid.Column="1" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
</DataTemplate>
|
||||
</ContentControl.ContentTemplate>
|
||||
</ContentControl>
|
||||
|
||||
</Grid>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class AgentsPageView : UserControl
|
||||
{
|
||||
public AgentsPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is AgentsPageViewModel vm)
|
||||
{
|
||||
vm.RequestAddAgentDialog += OnRequestAddAgentDialog;
|
||||
vm.RequestToolSettingsDialog += OnRequestToolSettingsDialog;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AddAgentResultData?> OnRequestAddAgentDialog(AddAgentViewModel vm)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is Window window)
|
||||
{
|
||||
var dialog = new AddAgentWindow { DataContext = vm };
|
||||
await dialog.ShowDialog(window);
|
||||
return vm.Result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Dictionary<string, object?>?> OnRequestToolSettingsDialog(ToolSettingsViewModel vm)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is Window window)
|
||||
{
|
||||
var dialog = new ToolSettingsWindow { DataContext = vm };
|
||||
await dialog.ShowDialog(window);
|
||||
return vm.Result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
|
||||
Padding="16"
|
||||
x:Class="ClawdDotNet.Desktop.Views.BackupPageView"
|
||||
x:DataType="vm:BackupPageViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Statusbar am unteren Rand -->
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="16">
|
||||
|
||||
<TextBlock Text="Sicherung & Wiederherstellung" Classes="heading" FontSize="20" />
|
||||
|
||||
<!-- Card: Neue Sicherung erstellen -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Manuelle Sicherung erstellen" Classes="heading" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto">
|
||||
<TextBox Text="{Binding TargetFolder}" Grid.Column="0" PlaceholderText="Zielordner für Sicherungen..." />
|
||||
<Button Content="Ordner wählen" Click="OnBrowseTargetFolder" Grid.Column="1" Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- Zugangsdaten Optionen -->
|
||||
<TextBlock Text="Zugangsdaten (API-Schlüssel, Passwörter):" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<RadioButton Content="Zugangsdaten NICHT mit sichern (ohne Passphrase)" IsChecked="{Binding SecretsExclude}" />
|
||||
<RadioButton Content="Mit Passphrase verschlüsseln" IsChecked="{Binding SecretsPassphrase}" />
|
||||
|
||||
<StackPanel Spacing="8" IsVisible="{Binding SecretsPassphrase}" Margin="20,0,0,0">
|
||||
<TextBox Text="{Binding Passphrase}" PasswordChar="*" PlaceholderText="Passphrase eingeben" Width="300" HorizontalAlignment="Left" />
|
||||
<TextBox Text="{Binding PassphraseRepeat}" PasswordChar="*" PlaceholderText="Passphrase wiederholen" Width="300" HorizontalAlignment="Left" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Inhalt Optionen -->
|
||||
<CheckBox Content="Protokolle (Logs) mit einschließen" IsChecked="{Binding IncludeLogs}" />
|
||||
<CheckBox Content="Chatverläufe mit einschließen" IsChecked="{Binding IncludeChatHistory}" />
|
||||
|
||||
<Button Content="▶ Sicherung jetzt erstellen" Command="{Binding CreateBackupCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" Classes="accent" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Card: Automatische Sicherung -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Automatische Sicherung" Classes="heading" />
|
||||
|
||||
<CheckBox Content="Automatische tägliche Sicherung aktivieren" IsChecked="{Binding AutoBackupEnabled}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="16" IsEnabled="{Binding AutoBackupEnabled}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Uhrzeit:" VerticalAlignment="Center" />
|
||||
<TextBox Text="{Binding AutoBackupTime}" Width="80" PlaceholderText="03:00" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="Max. Anzahl behalten:" VerticalAlignment="Center" />
|
||||
<NumericUpDown Value="{Binding KeepCount}" Minimum="1" Maximum="999" Width="120" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Vorhandene Sicherungen List -->
|
||||
<Border Classes="card">
|
||||
<DockPanel Height="260">
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
<TextBlock Text="Vorhandene Sicherungen" Classes="heading" VerticalAlignment="Center" />
|
||||
<Button Content="Aktualisieren" Command="{Binding RefreshBackupListCommand}" Margin="12,0,0,0" />
|
||||
<Button Content="Ordner öffnen" Command="{Binding ShowSelectedInFolderCommand}" IsEnabled="{Binding SelectedBackup, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<Button Content="Löschen" Command="{Binding DeleteSelectedCommand}" IsEnabled="{Binding SelectedBackup, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<Button Content="Wiederherstellen..." Command="{Binding RestoreSelectedCommand}" IsEnabled="{Binding SelectedBackup, Converter={x:Static ObjectConverters.IsNotNull}}" Classes="accent" />
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid ItemsSource="{Binding Backups}" SelectedItem="{Binding SelectedBackup}" AutoGenerateColumns="False" IsReadOnly="True">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Dateiname" Binding="{Binding FileName}" Width="220" />
|
||||
<DataGridTextColumn Header="Erstellt am" Binding="{Binding LastWriteTime, StringFormat='{}{0:g}'}" Width="150" />
|
||||
<DataGridTextColumn Header="Instanz" Binding="{Binding InstanceName}" Width="130" />
|
||||
<DataGridTextColumn Header="Größe" Binding="{Binding SizeFormatted}" Width="100" />
|
||||
<DataGridTextColumn Header="Zugangsdaten" Binding="{Binding SecretsInfo}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,56 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class BackupPageView : UserControl
|
||||
{
|
||||
public BackupPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is BackupPageViewModel vm)
|
||||
{
|
||||
vm.RequestRestoreDialog += OnRequestRestoreDialog;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<RestoreResultData?> OnRequestRestoreDialog(RestoreBackupViewModel vm)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is Window window)
|
||||
{
|
||||
var dialog = new RestoreBackupWindow { DataContext = vm };
|
||||
await dialog.ShowDialog(window);
|
||||
return vm.Result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async void OnBrowseTargetFolder(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not BackupPageViewModel vm) return;
|
||||
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel?.StorageProvider is null) return;
|
||||
|
||||
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Zielordner für Sicherungen wählen",
|
||||
AllowMultiple = false
|
||||
});
|
||||
|
||||
if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } path)
|
||||
{
|
||||
vm.TargetFolder = path;
|
||||
vm.SaveSettings();
|
||||
vm.RefreshBackupListCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
xmlns:conv="using:ClawdDotNet.Desktop.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
|
||||
Padding="16"
|
||||
x:Class="ClawdDotNet.Desktop.Views.ChatPageView"
|
||||
x:DataType="vm:ChatPageViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Statusleiste am unteren Rand -->
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<!-- Top Toolbar -->
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
<TextBlock Text="Agent:" VerticalAlignment="Center" FontWeight="Bold" />
|
||||
<ComboBox ItemsSource="{Binding AvailableAgents}" SelectedItem="{Binding SelectedAgent}" MinWidth="200">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<Button Content="Verlauf leeren" Command="{Binding ClearHistoryCommand}" Margin="12,0,0,0" />
|
||||
<Button Content="Abbrechen" Command="{Binding AbortCommand}" IsEnabled="{Binding IsSending}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Eingabezeile unten -->
|
||||
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="*,Auto" Margin="0,8,0,0">
|
||||
<TextBox Text="{Binding InputPrompt}" PlaceholderText="Nachricht eingeben... (Strg+Eingabe zum Senden)"
|
||||
AcceptsReturn="True" Height="60" TextWrapping="Wrap" Grid.Column="0" KeyDown="OnInputKeyDown" />
|
||||
<Button Content="Senden" Command="{Binding SendMessageCommand}" IsEnabled="{Binding !IsSending}"
|
||||
Classes="accent" VerticalAlignment="Stretch" Margin="8,0,0,0" Grid.Column="1" Width="90" />
|
||||
</Grid>
|
||||
|
||||
<!-- Nachrichtenverlauf (Mitte) -->
|
||||
<ScrollViewer x:Name="ChatScroller">
|
||||
<ItemsControl ItemsSource="{Binding ChatEntries}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Spacing="10" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:ChatEntryItemViewModel">
|
||||
<!-- Distinct styling: User right-aligned bubble vs Assistant left-aligned card -->
|
||||
<DockPanel HorizontalAlignment="{Binding IsUser, Converter={x:Static conv:ChatAlignmentConverter.Instance}}">
|
||||
<Border Classes="card" CornerRadius="8" MaxWidth="680"
|
||||
Background="{Binding IsUser, Converter={x:Static conv:ChatBubbleBrushConverter.Instance}, TargetNullValue={x:Null}}">
|
||||
<StackPanel Spacing="4">
|
||||
<DockPanel>
|
||||
<TextBlock Text="{Binding Sender}" FontWeight="Bold" FontSize="12" />
|
||||
<TextBlock Text="{Binding Timestamp, StringFormat='{}{0:HH:mm}'}" Classes="caption" HorizontalAlignment="Right" Margin="8,0,0,0" />
|
||||
</DockPanel>
|
||||
|
||||
<SelectableTextBlock Text="{Binding Text}" TextWrapping="Wrap" FontSize="13" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,25 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class ChatPageView : UserControl
|
||||
{
|
||||
public ChatPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnInputKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.Key == Key.Enter && e.KeyModifiers.HasFlag(KeyModifiers.Control))
|
||||
{
|
||||
if (DataContext is ChatPageViewModel vm && vm.SendMessageCommand.CanExecute(null))
|
||||
{
|
||||
vm.SendMessageCommand.Execute(null);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
|
||||
x:Class="ClawdDotNet.Desktop.Views.InfoPageView"
|
||||
x:DataType="vm:InfoPageViewModel">
|
||||
|
||||
<ScrollViewer Padding="16">
|
||||
<StackPanel Spacing="16">
|
||||
|
||||
<!-- Titel -->
|
||||
<TextBlock Text="Software- & Instanz-Informationen" Classes="heading" FontSize="20" />
|
||||
|
||||
<!-- Anwendungs-Details Card -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Anwendung" Classes="heading" />
|
||||
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="140,*">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Version:" FontWeight="Bold" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding AppVersion}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Build-Summary:" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding BuildSummary}" Margin="0,4,0,0" TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Instanz-Details Card -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Aktuelle Instanz" Classes="heading" />
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="140,*">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Name:" FontWeight="Bold" />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding InstanceName}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="ID:" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding InstanceId}" Margin="0,4,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Pfad:" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding InstancePath}" Margin="0,4,0,0" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Log-Ordner:" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding LogDirectory}" Margin="0,4,0,0" TextWrapping="Wrap" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="OpenRouter Status:" FontWeight="Bold" Margin="0,4,0,0" />
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="{Binding OpenRouterStatus}" Margin="0,4,0,0" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Assembly Build Info Card -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock Text="Module & Baugruppen" Classes="heading" />
|
||||
<DataGrid ItemsSource="{Binding Assemblies}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Modul" Binding="{Binding ShortName}" Width="160" />
|
||||
<DataGridTextColumn Header="Build #" Binding="{Binding BuildNumber}" Width="80" />
|
||||
<DataGridTextColumn Header="Build-Datum" Binding="{Binding BuildDate}" Width="160" />
|
||||
<DataGridTextColumn Header="Änderungen" Binding="{Binding Changes}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class InfoPageView : UserControl
|
||||
{
|
||||
public InfoPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
xmlns:models="using:ClawdDotNet.App.Models"
|
||||
mc:Ignorable="d" d:DesignWidth="720" d:DesignHeight="460"
|
||||
x:Class="ClawdDotNet.Desktop.Views.InstancePickerWindow"
|
||||
x:DataType="vm:InstancePickerViewModel"
|
||||
Icon="/Assets/app.ico"
|
||||
Title="ClawdDotNet – Instanz wählen"
|
||||
Width="720" Height="460"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<DockPanel Margin="16">
|
||||
|
||||
<TextBlock DockPanel.Dock="Top" Classes="heading" Text="Instanz wählen" />
|
||||
|
||||
<TextBlock DockPanel.Dock="Top" Classes="caption" Margin="0,0,0,12"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding InstancesDirectory, StringFormat='Verzeichnis: {0}'}" />
|
||||
|
||||
<!-- Fehlerzeile: nimmt keinen Platz weg, solange nichts anliegt -->
|
||||
<TextBlock DockPanel.Dock="Bottom"
|
||||
Margin="0,8,0,0"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding ErrorText}"
|
||||
IsVisible="{Binding ErrorText, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<!-- Aktionen -->
|
||||
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="*,Auto,Auto" Margin="0,12,0,0">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" Grid.Column="0">
|
||||
<TextBox Width="200"
|
||||
Watermark="Name der neuen Instanz"
|
||||
Text="{Binding NewInstanceName}" />
|
||||
<Button Content="Anlegen" Command="{Binding CreateCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button Grid.Column="1" Content="Abbrechen" Margin="0,0,6,0"
|
||||
Command="{Binding CancelCommand}" />
|
||||
|
||||
<Button Grid.Column="2" Content="Starten"
|
||||
Classes="accent"
|
||||
IsDefault="True"
|
||||
Command="{Binding StartCommand}" />
|
||||
</Grid>
|
||||
|
||||
<!-- Liste -->
|
||||
<DataGrid ItemsSource="{Binding Instances}"
|
||||
SelectedItem="{Binding Selected}"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Single"
|
||||
GridLinesVisibility="Horizontal">
|
||||
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Instanzname" Width="2*"
|
||||
Binding="{Binding InstanceName, x:DataType=models:InstanceInfo}" />
|
||||
<DataGridTextColumn Header="Ordner" Width="2*"
|
||||
Binding="{Binding FolderName, x:DataType=models:InstanceInfo}" />
|
||||
<DataGridTextColumn Header="Agenten" Width="Auto"
|
||||
Binding="{Binding AgentCount, x:DataType=models:InstanceInfo}" />
|
||||
<DataGridTextColumn Header="API-Key" Width="Auto"
|
||||
Binding="{Binding ApiKeyStatus, x:DataType=models:InstanceInfo}" />
|
||||
</DataGrid.Columns>
|
||||
|
||||
</DataGrid>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,24 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class InstancePickerWindow : Window
|
||||
{
|
||||
public InstancePickerWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Doppelklick auf eine Zeile startet — wie in der WinForms-Fassung. Das laesst
|
||||
// sich nicht sinnvoll als Befehl binden, weil DataGrid kein DoubleTapped je
|
||||
// Zeile anbietet; deshalb hier und nicht im Ansichtsmodell.
|
||||
AddHandler(DoubleTappedEvent, OnDoubleTapped, handledEventsToo: false);
|
||||
}
|
||||
|
||||
private void OnDoubleTapped(object? sender, TappedEventArgs e)
|
||||
{
|
||||
if (DataContext is InstancePickerViewModel vm && vm.StartCommand.CanExecute(null))
|
||||
vm.StartCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="520" d:DesignHeight="320"
|
||||
x:Class="ClawdDotNet.Desktop.Views.LicenseWindow"
|
||||
x:DataType="vm:LicenseViewModel"
|
||||
Icon="/Assets/app.ico"
|
||||
Title="ClawdDotNet – Lizenz"
|
||||
Width="520" SizeToContent="Height"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<StackPanel Margin="20" Spacing="12">
|
||||
|
||||
<TextBlock Classes="heading" Text="Lizenzschlüssel" />
|
||||
|
||||
<!-- Nur sichtbar, wenn es einen Grund gibt -->
|
||||
<Border Classes="card"
|
||||
IsVisible="{Binding Problem, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Problem}" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}" />
|
||||
</Border>
|
||||
|
||||
<TextBox Watermark="XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"
|
||||
Text="{Binding Key}"
|
||||
FontFamily="{DynamicResource ContentControlThemeFontFamily}" />
|
||||
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Classes="caption" Text="Hardware-ID (für den Support):" />
|
||||
<SelectableTextBlock Classes="caption"
|
||||
Text="{Binding HardwareId}"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right">
|
||||
<Button Content="Beenden" Command="{Binding CancelCommand}" />
|
||||
<Button Content="Prüfen" Classes="accent" IsDefault="True"
|
||||
Command="{Binding ConfirmCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,16 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class LicenseWindow : Window
|
||||
{
|
||||
public LicenseWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Wer das Fenster ueber das Kreuz schliesst, hat abgebrochen. Ohne das bliebe
|
||||
// der wartende Task fuer immer offen und der Start haenge.
|
||||
Closed += (_, _) => (DataContext as LicenseViewModel)?.CancelIfPending();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
xmlns:svc="using:ClawdDotNet.App.Services"
|
||||
xmlns:conv="using:ClawdDotNet.Desktop.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="600"
|
||||
x:Class="ClawdDotNet.Desktop.Views.LogPageView"
|
||||
x:DataType="vm:LogPageViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Werkzeugleiste -->
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
|
||||
<TextBlock Text="Modul:" VerticalAlignment="Center" />
|
||||
<ComboBox ItemsSource="{Binding Modules}"
|
||||
SelectedItem="{Binding SelectedModule}"
|
||||
MinWidth="180" />
|
||||
|
||||
<TextBlock Text="Stufe:" VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
<ComboBox ItemsSource="{Binding Levels}"
|
||||
SelectedItem="{Binding SelectedLevel}"
|
||||
MinWidth="100" />
|
||||
|
||||
<Button Content="Aktualisieren" Command="{Binding RefreshModulesCommand}" Margin="12,0,0,0" />
|
||||
<Button Content="Leeren" Command="{Binding ClearCommand}" />
|
||||
<Button Content="Ordner öffnen" Command="{Binding OpenFolderCommand}" />
|
||||
|
||||
<CheckBox Content="Mitlaufen" IsChecked="{Binding FollowTail}"
|
||||
VerticalAlignment="Center" Margin="12,0,0,0" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
SelectableTextBlock je Zeile statt eines grossen TextBlocks: So laesst sich eine
|
||||
einzelne Zeile herauskopieren, und die Virtualisierung des ItemsControl haelt auch
|
||||
bei 2000 Zeilen die Darstellung fluessig.
|
||||
-->
|
||||
<ScrollViewer Name="Scroller" Padding="8">
|
||||
<ItemsControl ItemsSource="{Binding Lines}">
|
||||
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<VirtualizingStackPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="svc:LogLine">
|
||||
<SelectableTextBlock Text="{Binding Text}"
|
||||
FontFamily="Consolas, Menlo, DejaVu Sans Mono, monospace"
|
||||
FontSize="12"
|
||||
TextWrapping="NoWrap"
|
||||
Foreground="{Binding Level,
|
||||
Converter={x:Static conv:LogLevelBrushConverter.Instance},
|
||||
TargetNullValue={x:Null}}" />
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class LogPageView : UserControl
|
||||
{
|
||||
public LogPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
// Mitlaufen: Nach jeder neuen Zeile ans Ende springen — aber nur, solange der
|
||||
// Haken gesetzt ist. Das gehoert in die Ansicht, weil nur sie den ScrollViewer
|
||||
// kennt; das Ansichtsmodell haelt lediglich den Schalter.
|
||||
Scroller.ScrollChanged += (_, _) =>
|
||||
{
|
||||
if (DataContext is LogPageViewModel { FollowTail: true })
|
||||
Scroller.ScrollToEnd();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="1100" d:DesignHeight="700"
|
||||
x:Class="ClawdDotNet.Desktop.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Icon="/Assets/app.ico"
|
||||
Title="ClawdDotNet"
|
||||
Width="1100" Height="700"
|
||||
MinWidth="800" MinHeight="500">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Haupt-Menüleiste -->
|
||||
<Menu DockPanel.Dock="Top">
|
||||
<MenuItem Header="_Datei">
|
||||
<MenuItem Header="_Beenden" Command="{Binding ExitCommand}" InputGesture="Alt+F4" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Bearbeiten">
|
||||
<MenuItem Header="_Instanz-Ordner öffnen" Command="{Binding OpenInstanceFolderCommand}" />
|
||||
<MenuItem Header="_Log-Ordner öffnen" Command="{Binding OpenLogFolderCommand}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Instanz">
|
||||
<MenuItem Header="_Instanz-Manager..." Command="{Binding OpenInstanceManagerCommand}" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Ansicht">
|
||||
<MenuItem Header="_Chat" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Chat" />
|
||||
<MenuItem Header="_Logs" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Logs" />
|
||||
<MenuItem Header="_Agenten" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Agenten" />
|
||||
<MenuItem Header="_Einstellungen" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Einstellungen" />
|
||||
<MenuItem Header="_Aufgaben" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Aufgaben" />
|
||||
<MenuItem Header="_Sicherung" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Sicherung" />
|
||||
<MenuItem Header="_Info" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Info" />
|
||||
</MenuItem>
|
||||
<MenuItem Header="_Hilfe">
|
||||
<MenuItem Header="_Info" Command="{Binding SelectPageByTitleCommand}" CommandParameter="Info" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<!-- Statusleiste -->
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<TextBlock Classes="caption" Text="{Binding StatusText}" />
|
||||
</Border>
|
||||
|
||||
<!-- Bereiche (Seitenleiste links) -->
|
||||
<TabControl ItemsSource="{Binding Pages}"
|
||||
SelectedItem="{Binding SelectedPage}"
|
||||
TabStripPlacement="Left">
|
||||
|
||||
<TabControl.ItemTemplate>
|
||||
<DataTemplate DataType="vm:PageViewModel">
|
||||
<TextBlock Text="{Binding Title}" Margin="4,8" />
|
||||
</DataTemplate>
|
||||
</TabControl.ItemTemplate>
|
||||
|
||||
<TabControl.ContentTemplate>
|
||||
<DataTemplate DataType="vm:PageViewModel">
|
||||
<ContentControl Content="{Binding}" />
|
||||
</DataTemplate>
|
||||
</TabControl.ContentTemplate>
|
||||
|
||||
</TabControl>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,28 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is MainWindowViewModel vm)
|
||||
{
|
||||
vm.RequestExit += Close;
|
||||
vm.RequestInstanceManagerDialog += OnOpenInstanceManager;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnOpenInstanceManager()
|
||||
{
|
||||
var dialog = new InstancePickerWindow();
|
||||
await dialog.ShowDialog(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="400"
|
||||
x:Class="ClawdDotNet.Desktop.Views.PlaceholderPageView"
|
||||
x:DataType="vm:PlaceholderPageViewModel">
|
||||
|
||||
<Border Classes="card" Margin="16" VerticalAlignment="Top">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="heading" Text="{Binding Title}" />
|
||||
<TextBlock Classes="caption" Text="{Binding Note}" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="caption" Margin="0,8,0,0"
|
||||
Text="Dieser Bereich ist noch nicht portiert." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class PlaceholderPageView : UserControl
|
||||
{
|
||||
public PlaceholderPageView() => InitializeComponent();
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
|
||||
Width="640" Height="420"
|
||||
Padding="16"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
Title="Sicherung wiederherstellen"
|
||||
x:Class="ClawdDotNet.Desktop.Views.RestoreBackupWindow"
|
||||
x:DataType="vm:RestoreBackupViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Buttons unten -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,16,0,0">
|
||||
<Button Content="Abbrechen" Command="{Binding CancelCommand}" />
|
||||
<Button Content="Wiederherstellen" Command="{Binding ConfirmCommand}" Classes="accent" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Statusmeldung unten -->
|
||||
<TextBlock DockPanel.Dock="Bottom" Text="{Binding StatusMessage}" Foreground="Red" Margin="0,8,0,0" TextWrapping="Wrap" />
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<!-- Information -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Sicherungsinformationen" Classes="heading" />
|
||||
<TextBlock Text="{Binding InstanceName, StringFormat='Instanz: {0}'}" />
|
||||
<TextBlock Text="{Binding CreatedAt, StringFormat='Erstellt: {0:g}'}" />
|
||||
<TextBlock Text="{Binding FileCount, StringFormat='Dateien: {0}'}" />
|
||||
<TextBlock Text="{Binding SecretsDescription, StringFormat='Zugangsdaten: {0}'}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Zielordner -->
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Zielordner:" FontWeight="Bold" />
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBox Text="{Binding TargetDirectory}" Grid.Column="0" />
|
||||
<Button Content="Ordner wählen" Click="OnBrowseFolder" Grid.Column="1" Margin="8,0,0,0" />
|
||||
</Grid>
|
||||
<TextBlock Text="Hinweis: Es wird empfohlen, in einen neuen Ordner wiederherzustellen, um Konflikte mit laufenden Prozessen zu vermeiden."
|
||||
Classes="caption" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Passphrase -->
|
||||
<StackPanel Spacing="4" IsVisible="{Binding NeedsPassphrase}">
|
||||
<TextBlock Text="Passphrase:" FontWeight="Bold" />
|
||||
<TextBox Text="{Binding Passphrase}" PasswordChar="*" />
|
||||
<TextBlock Text="Diese Sicherung enthält geschützte Zugangsdaten." Classes="caption" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Überschreiben -->
|
||||
<CheckBox Content="Vorhandene Dateien im Zielordner überschreiben" IsChecked="{Binding Overwrite}" Margin="0,8,0,0" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,42 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class RestoreBackupWindow : Window
|
||||
{
|
||||
public RestoreBackupWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is RestoreBackupViewModel vm)
|
||||
{
|
||||
vm.CloseRequested += () => Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnBrowseFolder(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (DataContext is not RestoreBackupViewModel vm) return;
|
||||
|
||||
var storage = StorageProvider;
|
||||
if (storage is null) return;
|
||||
|
||||
var folders = await storage.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Zielordner für Wiederherstellung wählen",
|
||||
AllowMultiple = false
|
||||
});
|
||||
|
||||
if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } path)
|
||||
{
|
||||
vm.TargetDirectory = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
|
||||
Padding="16"
|
||||
x:Class="ClawdDotNet.Desktop.Views.SettingsPageView"
|
||||
x:DataType="vm:SettingsPageViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Statusbar am unteren Rand -->
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||
<Button Content="Verwerfen / Zurücksetzen" Command="{Binding LoadFromHostCommand}" />
|
||||
<Button Content="Speichern" Command="{Binding SaveSettingsCommand}" Classes="accent" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="16">
|
||||
|
||||
<TextBlock Text="Anwendungs- & Instanz-Einstellungen" Classes="heading" FontSize="20" />
|
||||
|
||||
<!-- Card: Instanz-Einstellungen -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Aktuelle Instanz" Classes="heading" />
|
||||
|
||||
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Instanz-Name:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding InstanceName}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="OpenRouter API-Key:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding OpenRouterApiKey}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Tagesbudget ($ USD):" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding InstanceDailyCostUsd}" Minimum="0" Maximum="10000" FormatString="F2" Width="140" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Tagesbudget (Tokens):" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding InstanceDailyTokens}" Minimum="0" Maximum="100000000" FormatString="0" Width="180" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Card: Allgemein & Pfade -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Allgemein & Pfade" Classes="heading" />
|
||||
|
||||
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Log-Verzeichnis:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding LogDirectory}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Instanzen-Verzeichnis:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InstancesDirectory}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Minimaler Log-Level:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding LogLevels}" SelectedItem="{Binding MinimumLogLevel}" Width="140" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="OpenRouter Base-URL:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding OpenRouterBaseUrl}" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Card: Deploymentcenter & Lizenz -->
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Deploymentcenter & Lizenz" Classes="heading" />
|
||||
|
||||
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Server-URL:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding DeploymentcenterUrl}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Master-Token:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding DeploymentcenterToken}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Update-Kanal:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding UpdateChannels}" SelectedItem="{Binding UpdateChannel}" Width="140" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Umgebung:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="3" Grid.Column="1" ItemsSource="{Binding Environments}" SelectedItem="{Binding DeploymentcenterEnvironment}" Width="160" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Lizenzschlüssel:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding LicenseKey}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<CheckBox Content="Fehler automatisch an Deploymentcenter melden" IsChecked="{Binding ErrorReportingEnabled}" Margin="0,4,0,0" />
|
||||
<CheckBox Content="Beim Start automatisch auf Updates prüfen" IsChecked="{Binding UpdateCheckEnabled}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class SettingsPageView : UserControl
|
||||
{
|
||||
public SettingsPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700"
|
||||
Padding="16"
|
||||
x:Class="ClawdDotNet.Desktop.Views.TasksPageView"
|
||||
x:DataType="vm:TasksPageViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Statusbar unten -->
|
||||
<Border Classes="statusbar" DockPanel.Dock="Bottom">
|
||||
<TextBlock Text="{Binding StatusText}" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
|
||||
<TabControl>
|
||||
|
||||
<!-- TAB 1: Jobs (Scheduled / Worker) -->
|
||||
<TabItem Header="Geplante Jobs">
|
||||
<DockPanel Margin="0,8,0,0">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
<Button Content="Job hinzufügen" Command="{Binding AddJobCommand}" Classes="accent" />
|
||||
<Button Content="Bearbeiten" Command="{Binding EditJobCommand}" IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<Button Content="Entfernen" Command="{Binding RemoveJobCommand}" IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<Button Content="▶ Jetzt ausführen" Command="{Binding RunJobNowCommand}" IsEnabled="{Binding SelectedJob, Converter={x:Static ObjectConverters.IsNotNull}}" Margin="12,0,0,0" />
|
||||
<Button Content="Aktualisieren" Command="{Binding RefreshJobListCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Jobs DataGrid -->
|
||||
<DataGrid ItemsSource="{Binding JobEntries}" SelectedItem="{Binding SelectedJob}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Typ" Binding="{Binding JobType}" Width="120" />
|
||||
<DataGridTextColumn Header="Agent" Binding="{Binding AgentName}" Width="150" />
|
||||
<DataGridTextColumn Header="Tool / Nachricht" Binding="{Binding TaskMessage}" Width="*" />
|
||||
<DataGridTextColumn Header="Cron-Ausdruck" Binding="{Binding CronExpression}" Width="120" />
|
||||
<DataGridTextColumn Header="Nächster Lauf" Binding="{Binding NextRun}" Width="140" />
|
||||
<DataGridTextColumn Header="Letzter Lauf" Binding="{Binding LastRun}" Width="140" />
|
||||
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- TAB 2: Dienste (Services) -->
|
||||
<TabItem Header="Dienste (Services)">
|
||||
<DockPanel Margin="0,8,0,0">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
<Button Content="Dienst hinzufügen" Command="{Binding AddServiceCommand}" Classes="accent" />
|
||||
<Button Content="Entfernen" Command="{Binding RemoveServiceCommand}" IsEnabled="{Binding SelectedService, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||
<Button Content="Aktualisieren" Command="{Binding RefreshServiceListCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Services DataGrid -->
|
||||
<DataGrid ItemsSource="{Binding ServiceEntries}" SelectedItem="{Binding SelectedService}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Dienstname" Binding="{Binding Name}" Width="180" />
|
||||
<DataGridTextColumn Header="Typ" Binding="{Binding Type}" Width="100" />
|
||||
<DataGridTextColumn Header="Port" Binding="{Binding Port}" Width="80" />
|
||||
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="110" />
|
||||
<DataGridTextColumn Header="Beschreibung" Binding="{Binding Description}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
<!-- TAB 3: Verlauf (Job History) -->
|
||||
<TabItem Header="Verlauf (History)">
|
||||
<DockPanel Margin="0,8,0,0">
|
||||
|
||||
<!-- Toolbar -->
|
||||
<StackPanel Classes="toolbar" DockPanel.Dock="Top">
|
||||
<Button Content="Verlauf aktualisieren" Command="{Binding RefreshJobHistoryCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- History DataGrid -->
|
||||
<DataGrid ItemsSource="{Binding JobHistoryEntries}" AutoGenerateColumns="False" IsReadOnly="True" GridLinesVisibility="Horizontal">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Zeitpunkt" Binding="{Binding Time, StringFormat='{}{0:g}'}" Width="140" />
|
||||
<DataGridTextColumn Header="Job-Name" Binding="{Binding JobName}" Width="140" />
|
||||
<DataGridTextColumn Header="Agent" Binding="{Binding Agent}" Width="140" />
|
||||
<DataGridTextColumn Header="Beschreibung" Binding="{Binding JobDescription}" Width="220" />
|
||||
<DataGridTextColumn Header="Status" Binding="{Binding Status}" Width="100" />
|
||||
<DataGridTextColumn Header="Details / Info" Binding="{Binding Info}" Width="*" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</DockPanel>
|
||||
</TabItem>
|
||||
|
||||
</TabControl>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</UserControl>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class TasksPageView : UserControl
|
||||
{
|
||||
public TasksPageView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is TasksPageViewModel vm)
|
||||
{
|
||||
vm.RequestAddJobDialog += OnRequestAddJobDialog;
|
||||
vm.RequestAddServiceDialog += OnRequestAddServiceDialog;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AddJobResultData?> OnRequestAddJobDialog(AddJobViewModel vm)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is Window window)
|
||||
{
|
||||
var dialog = new AddJobWindow { DataContext = vm };
|
||||
await dialog.ShowDialog(window);
|
||||
return vm.Result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<AddServiceResultData?> OnRequestAddServiceDialog(AddServiceViewModel vm)
|
||||
{
|
||||
var topLevel = TopLevel.GetTopLevel(this);
|
||||
if (topLevel is Window window)
|
||||
{
|
||||
var dialog = new AddServiceWindow { DataContext = vm };
|
||||
await dialog.ShowDialog(window);
|
||||
return vm.Result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ClawdDotNet.Desktop.ViewModels"
|
||||
xmlns:models="using:ClawdDotNet.Models"
|
||||
xmlns:conv="using:ClawdDotNet.Desktop.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="560" d:DesignHeight="480"
|
||||
Width="580" Height="500"
|
||||
Padding="16"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
CanResize="False"
|
||||
Title="Werkzeug-Einstellungen"
|
||||
x:Class="ClawdDotNet.Desktop.Views.ToolSettingsWindow"
|
||||
x:DataType="vm:ToolSettingsViewModel">
|
||||
|
||||
<DockPanel>
|
||||
|
||||
<!-- Bottom Buttons -->
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8" Margin="0,16,0,0">
|
||||
<Button Content="Abbrechen" Command="{Binding CancelCommand}" />
|
||||
<Button Content="Speichern" Command="{Binding ConfirmCommand}" Classes="accent" />
|
||||
</StackPanel>
|
||||
|
||||
<ScrollViewer>
|
||||
<StackPanel Spacing="12">
|
||||
|
||||
<TextBlock Text="{Binding ToolName, StringFormat='Einstellungen für {0}'}" Classes="heading" FontSize="18" />
|
||||
|
||||
<!-- Forms for each Tool -->
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<!-- 1. FTP -->
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=FTP}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Host:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FtpHost}" PlaceholderText="ftp.example.com" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Port:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding FtpPort}" Minimum="1" Maximum="65535" Width="120" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Benutzername:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding FtpUsername}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Passwort:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding FtpPassword}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="Root-Pfad:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding FtpRootPath}" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 2. Mail -->
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=Mail}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Benutzername:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding MailUsername}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Passwort:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding MailPassword}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="IMAP-Host:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding MailImapHost}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="IMAP-Port:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding MailImapPort}" Minimum="1" Maximum="65535" Width="120" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="SMTP-Host:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding MailSmtpHost}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="SMTP-Port:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="5" Grid.Column="1" Value="{Binding MailSmtpPort}" Minimum="1" Maximum="65535" Width="120" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" Text="Erlaubte Empfänger:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding MailAllowedRecipients}" PlaceholderText="email1@domain.de, email2@domain.de" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 3. Database -->
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=Database}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Typ:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding DbTypes}" SelectedItem="{Binding DbType}" HorizontalAlignment="Stretch" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Connection-String:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding DbConnectionString}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Zugriffsebene:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding DbAccessLevels}" SelectedItem="{Binding DbAccessLevel}" HorizontalAlignment="Stretch" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Erlaubte Tabellen:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding DbAllowedTables}" PlaceholderText="tabelle1, tabelle2" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 4. Telegram -->
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=Telegram}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Bot-Token:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding TelegramBotToken}" PasswordChar="*" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Standard Chat-ID:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding TelegramDefaultChatId}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Erlaubte Chat-IDs:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding TelegramAllowedChatIds}" PlaceholderText="id1, id2" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 5. DirectAPI -->
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=DirectAPI}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Standard-Provider:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding DirectApiDefaultProvider}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Cache TTL (Sek.):" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding DirectApiCacheTtlSeconds}" Minimum="0" Maximum="86400" Width="120" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Twelve Data Key:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding DirectApiTwelveDataKey}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Alpha Vantage Key:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding DirectApiAlphaVantageKey}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 6. WebFetch -->
|
||||
<Grid ColumnDefinitions="140,*" RowDefinitions="Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=WebFetch}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Erlaubte Domains:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding WebFetchAllowedDomains}" PlaceholderText="reuters.com, bloomberg.com" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Max Response KB:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding WebFetchMaxResponseKb}" Minimum="1" Maximum="10000" Width="120" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="User Agent:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding WebFetchUserAgent}" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 7. SocialMediaManager -->
|
||||
<Grid ColumnDefinitions="160,*" RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=SocialMediaManager}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="X (Twitter) API-Key:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding SmmXApiKey}" PasswordChar="*" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="X Accounts:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding SmmXWatchAccounts}" PlaceholderText="elonmusk, unusual_whales" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Subreddits:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding SmmRedditWatchSubreddits}" PlaceholderText="wallstreetbets, stocks" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Reddit Post Limit:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding SmmRedditPostLimit}" Minimum="1" Maximum="100" Width="120" HorizontalAlignment="Left" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="OpenRouter Key:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding SmmOpenRouterApiKey}" PasswordChar="*" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="STT Modell:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding SmmSttModel}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="6" Grid.Column="0" Text="YouTube Kanäle:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding SmmYoutubeChannels}" PlaceholderText="URL1, URL2" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- 8. FileRW -->
|
||||
<Grid ColumnDefinitions="180,*" RowDefinitions="Auto,Auto,Auto,Auto" IsVisible="{Binding ToolName, Converter={x:Static conv:ToolMatchConverter.Instance}, ConverterParameter=FileRW}">
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Personal Allowed Ext:" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding FileRwPersonalAllowedExtensions}" />
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Shared Access Level:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<ComboBox Grid.Row="1" Grid.Column="1" ItemsSource="{Binding FileRwAccessLevels}" SelectedItem="{Binding FileRwSharedAccessLevel}" HorizontalAlignment="Stretch" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Shared Allowed Ext:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding FileRwSharedAllowedExtensions}" Margin="0,8,0,0" />
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Geschützte Pfade:" FontWeight="Bold" VerticalAlignment="Center" Margin="0,8,0,0" />
|
||||
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding FileRwProtectedPaths}" Margin="0,8,0,0" />
|
||||
</Grid>
|
||||
|
||||
<!-- Fallback info for tools without extra config -->
|
||||
<TextBlock Text="Dieses Werkzeug benötigt keine weiteren Einstellungen."
|
||||
IsVisible="{Binding SettingsObject, Converter={x:Static ObjectConverters.IsNull}}"
|
||||
FontStyle="Italic" />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
</DockPanel>
|
||||
|
||||
</Window>
|
||||
@@ -0,0 +1,21 @@
|
||||
using Avalonia.Controls;
|
||||
using ClawdDotNet.Desktop.ViewModels;
|
||||
|
||||
namespace ClawdDotNet.Desktop.Views;
|
||||
|
||||
public partial class ToolSettingsWindow : Window
|
||||
{
|
||||
public ToolSettingsWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is ToolSettingsViewModel vm)
|
||||
{
|
||||
vm.CloseRequested += () => Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user