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; } record Suggestion(string Title, string Subtitle, string Prompt); class MainViewModel : ObservableObject { static readonly ModelOption Default = new("Predefinito", ""); static readonly ModelOption Custom = new("Altro ID…", "__custom"); public ObservableCollection Messages { get; } = []; public ObservableCollection Chats { get; } = []; public ModelOption[] ModelChoices { get; } = [Default, .. Catalog.Models, Custom]; public string[] EffortChoices { get; } = ["Predefinito", .. Catalog.Efforts]; public Suggestion[] Suggestions { get; } = [ new("Spiega il progetto", "Panoramica e struttura", "Spiega cosa fa questo progetto e come è organizzato."), new("Scrivi uno script", "PowerShell, Python, C#…", "Scrivi uno script PowerShell che "), new("Rivedi del codice", "Bug, leggibilità, prestazioni", "Rivedi questo codice e suggerisci miglioramenti:\n\n"), new("Riassumi un testo", "Punti chiave in breve", "Riassumi in modo chiaro questo testo:\n\n"), ]; public ICommand NewChatCommand => newChat; public ICommand CopyCommand { get; } public ICommand SendCommand { get; } public ICommand OpenCommand { get; } public ICommand DeleteCommand { get; } public ICommand SuggestCommand { get; } public ICommand ToggleSidebarCommand { get; } /// Conferma di eliminazione, fornita dalla finestra. /// Copia negli appunti, fornita dalla finestra. public Func CopyText { get; set; } = _ => Task.CompletedTask; public Func> ConfirmDelete { get; set; } = () => Task.FromResult(true); /// Chiesto alla finestra di rimettere il fuoco sul campo messaggio. public event Action? FocusInput; string sessionId = Guid.NewGuid().ToString(); bool started; // true dopo il primo messaggio riuscito: da lì in poi --resume List history = []; string input = ""; bool busy, sidebarOpen = true; ModelOption selectedModel = Default; string customModel = ""; string selectedEffort = "Predefinito"; readonly RelayCommand send, newChat; public MainViewModel() { send = new(_ => _ = SendAsync(), () => CanSend); SendCommand = send; newChat = new(_ => NewChat(), () => !busy); CopyCommand = new RelayCommand(t => { if (t is not null) _ = CopyText(t); }); OpenCommand = new RelayCommand(c => { if (c is not null) OpenChat(c.Id); }); DeleteCommand = new RelayCommand(c => { if (c is not null) _ = DeleteAsync(c); }); SuggestCommand = new RelayCommand(s => { if (s is not null) { Input = s.Prompt; FocusInput?.Invoke(); } }); ToggleSidebarCommand = new RelayCommand(_ => 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 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 } }