feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -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;
|
||||
Reference in New Issue
Block a user