Files
agent-wiki-ui/Gui/MainViewModel.cs
T
davideandClaude Sonnet 5.5 fee788134d GUI: impostazioni di modello ed effort in un riquadro in alto a destra
Modello ed effort lasciano il composer e passano in un riquadro a comparsa
aperto dal pulsante in alto a destra, simmetrico a quello della barra
laterale. Il composer resta con campo di testo e pulsante Invia sulla
stessa riga. Eliminati i suggerimenti della schermata iniziale. MVP.md e la
matrice di TESTING.md sono aggiornati di conseguenza.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-15 11:26:06 +02:00

169 lines
6.1 KiB
C#

using System.Collections.ObjectModel;
using System.Windows.Input;
using AgentWikiUi.Core;
namespace AgentWikiUi.Gui;
class MessageVm(string role, string text)
{
public string Role { get; } = role;
public string Text { get; } = text;
public bool IsUser => Role == "user";
public bool IsAi => Role == "ai";
public bool IsErr => Role == "err";
}
class ChatItem(string id, string title, DateTime updated, bool active)
{
public string Id { get; } = id;
public string Title { get; } = title;
public string When { get; } = updated.ToString("d MMM HH:mm");
public bool IsActive { get; } = active;
}
class MainViewModel : ObservableObject
{
static readonly ModelOption Default = new("Predefinito", "");
static readonly ModelOption Custom = new("Altro ID…", "__custom");
public ObservableCollection<MessageVm> Messages { get; } = [];
public ObservableCollection<ChatItem> Chats { get; } = [];
public ModelOption[] ModelChoices { get; } = [Default, .. Catalog.Models, Custom];
public string[] EffortChoices { get; } = ["Predefinito", .. Catalog.Efforts];
public ICommand NewChatCommand => newChat;
public ICommand CopyCommand { get; }
public ICommand SendCommand { get; }
public ICommand OpenCommand { get; }
public ICommand DeleteCommand { get; }
public ICommand ToggleSidebarCommand { get; }
/// <summary>Conferma di eliminazione, fornita dalla finestra.</summary>
/// <summary>Copia negli appunti, fornita dalla finestra.</summary>
public Func<string, Task> CopyText { get; set; } = _ => Task.CompletedTask;
public Func<Task<bool>> ConfirmDelete { get; set; } = () => Task.FromResult(true);
/// <summary>Chiesto alla finestra di rimettere il fuoco sul campo messaggio.</summary>
public event Action? FocusInput;
string sessionId = Guid.NewGuid().ToString();
bool started; // true dopo il primo messaggio riuscito: da lì in poi --resume
List<Msg> history = [];
string input = "";
bool busy, sidebarOpen = true;
ModelOption selectedModel = Default;
string customModel = "";
string selectedEffort = "Predefinito";
readonly RelayCommand<object> send, newChat;
public MainViewModel()
{
send = new(_ => _ = SendAsync(), () => CanSend);
SendCommand = send;
newChat = new(_ => NewChat(), () => !busy);
CopyCommand = new RelayCommand<string>(t => { if (t is not null) _ = CopyText(t); });
OpenCommand = new RelayCommand<ChatItem>(c => { if (c is not null) OpenChat(c.Id); });
DeleteCommand = new RelayCommand<ChatItem>(c => { if (c is not null) _ = DeleteAsync(c); });
ToggleSidebarCommand = new RelayCommand<object>(_ => SidebarOpen = !SidebarOpen);
Messages.CollectionChanged += (_, _) => { Raise(nameof(HasMessages)); Raise(nameof(ShowEmpty)); };
// all'avvio riapre la chat più recente
if (ChatStore.List().FirstOrDefault() is { } last) OpenChat(last.Id); else RefreshChats();
}
public bool HasMessages => Messages.Count > 0;
public bool ShowEmpty => Messages.Count == 0;
public bool CanSend => !busy && !string.IsNullOrWhiteSpace(input);
public string Input
{
get => input;
set { if (Set(ref input, value)) { Raise(nameof(CanSend)); send.Refresh(); } }
}
public bool IsBusy
{
get => busy;
private set { if (Set(ref busy, value)) { Raise(nameof(CanSend)); send.Refresh(); newChat.Refresh(); } }
}
public bool SidebarOpen { get => sidebarOpen; set => Set(ref sidebarOpen, value); }
public ModelOption SelectedModel
{
get => selectedModel;
set { if (Set(ref selectedModel, value)) Raise(nameof(IsCustomModel)); }
}
public bool IsCustomModel => selectedModel == Custom;
public string CustomModel { get => customModel; set => Set(ref customModel, value); }
public string SelectedEffort { get => selectedEffort; set => Set(ref selectedEffort, value); }
void RefreshChats()
{
Chats.Clear();
foreach (var c in ChatStore.List()) Chats.Add(new(c.Id, c.Title, c.Updated, c.Id == sessionId));
}
void Show(List<Msg> msgs)
{
Messages.Clear();
foreach (var m in msgs) Messages.Add(new(m.Role, m.Text));
}
void NewChat()
{
sessionId = Guid.NewGuid().ToString(); started = false; history = [];
Show(history); RefreshChats(); FocusInput?.Invoke();
}
void OpenChat(string id)
{
if (busy) return;
if (ChatStore.Load(id) is not { } chat) { RefreshChats(); return; }
sessionId = chat.Id; started = chat.Started; history = chat.Messages;
Show(history); RefreshChats();
}
async Task DeleteAsync(ChatItem c)
{
if (busy || !await ConfirmDelete()) return;
ChatStore.Delete(c.Id);
if (c.Id == sessionId) NewChat(); else RefreshChats();
}
async Task SendAsync()
{
if (!CanSend) return;
var text = input.Trim();
var model = IsCustomModel ? customModel.Trim() : selectedModel.Id;
var effort = selectedEffort == "Predefinito" ? "" : selectedEffort;
Input = ""; IsBusy = true;
Add("user", text);
Persist(); // il messaggio non va perso se l'app si chiude durante l'attesa
RefreshChats();
try
{
var r = await Claude.RunCapturedAsync(text, sessionId, started, model, effort);
if (r.Code == 0) { started = true; Add("ai", r.Out); }
else Add("err", $"claude è uscito con codice {r.Code}\n{r.Err}".Trim());
}
catch (System.ComponentModel.Win32Exception) { Add("err", "'claude' non trovato nel PATH."); }
catch (Exception ex) { Add("err", ex.Message); }
finally
{
IsBusy = false;
Persist();
RefreshChats();
FocusInput?.Invoke();
}
}
void Add(string role, string text) { history.Add(new(role, text)); Messages.Add(new(role, text)); }
void Persist()
{
try { ChatStore.Save(sessionId, started, history); }
catch (IOException) { } // la chat resta a schermo anche se il salvataggio fallisce
}
}