using System.ComponentModel; using System.Text.Json; using NucleicApp.Services; namespace NucleicApp.ViewModels; /// Presentation state for the first WinUI Home screen. It derives every value from /// ``RendererStore`` projections; it never mutates host-owned state. public sealed class HomeViewModel : ViewModelBase, IDisposable { private readonly RendererStore store; private readonly Func now; public HomeViewModel(RendererStore store, Func? now = null) { this.store = store; this.now = now ?? (() => DateTimeOffset.Now); store.PropertyChanged += StoreChanged; } public string Greeting => now().Hour switch { >= 5 and < 12 => "Good morning", >= 12 and < 17 => "Good afternoon", >= 17 and < 22 => "Good evening", _ => "Hello", }; public DashboardCounts Counts { get { if (store.Dashboard is not { } dashboard || !dashboard.TryGetProperty("counts", out var counts)) return new(); return new DashboardCounts( Int(counts, "projects"), Int(counts, "activeChats"), Int(counts, "chats"), Int(counts, "messages"), Long(counts, "tokens")); } } public string Subtitle => Counts.Chats == 0 ? "Start a chat below to get going." : $"{Counts.ActiveChats} active of {Counts.Chats} chats ยท {Counts.Projects} " + (Counts.Projects == 1 ? "project" : "projects"); public IReadOnlyList LiveSessions => store.Sessions .Where(session => !Bool(session.Raw, "archived")) .OrderByDescending(UpdatedAt) .ToArray(); public IReadOnlyList NeedsAttention => LiveSessions .Where(session => session.PendingApprovalCount > 0 || session.Status == "awaitingApproval" || (session.Status == "awaitingInput" && String(session.Raw, "disposition") == "awaitingInput")) .ToArray(); public IReadOnlyList InProgress => LiveSessions .Where(session => session.Status is "provisioning" or "running") .ToArray(); public int OpenTodoCount { get { if (store.Dashboard is not { } dashboard || !dashboard.TryGetProperty("todos", out var todos) || todos.ValueKind != JsonValueKind.Array) return 0; return todos.EnumerateArray().Count(todo => String(todo, "status") != "done"); } } private void StoreChanged(object? sender, PropertyChangedEventArgs args) { if (args.PropertyName == nameof(RendererStore.Sessions)) { Raise(nameof(LiveSessions)); Raise(nameof(NeedsAttention)); Raise(nameof(InProgress)); } else if (args.PropertyName == nameof(RendererStore.Dashboard)) { Raise(nameof(Counts)); Raise(nameof(Subtitle)); Raise(nameof(OpenTodoCount)); } } private static bool Bool(JsonElement root, string name) => root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.True; private static string? String(JsonElement root, string name) => root.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String ? value.GetString() : null; private static int Int(JsonElement root, string name) => root.TryGetProperty(name, out var value) && value.TryGetInt32(out var result) ? result : 0; private static long Long(JsonElement root, string name) => root.TryGetProperty(name, out var value) && value.TryGetInt64(out var result) ? result : 0; private static double UpdatedAt(SessionSummary session) => session.Raw.TryGetProperty("updatedAt", out var value) && value.TryGetDouble(out var result) ? result : 0; public void Dispose() => store.PropertyChanged -= StoreChanged; } public sealed record DashboardCounts( int Projects = 0, int ActiveChats = 0, int Chats = 0, int Messages = 0, long Tokens = 0);