diff --git a/.agent/skills/nexus-ui-engine/SKILL.md b/.agent/skills/nexus-ui-engine/SKILL.md index 4c5eba8..988b731 100644 --- a/.agent/skills/nexus-ui-engine/SKILL.md +++ b/.agent/skills/nexus-ui-engine/SKILL.md @@ -12,14 +12,22 @@ description: Design System & Component rules for Blazor - **Styling & Isolation:** - Mandatory use of scoped CSS (`.razor.css`). + - Strict compliance: Zero inline ` diff --git a/src/NexusReader.UI.Shared/Layout/AuthLayout.razor.css b/src/NexusReader.UI.Shared/Layout/AuthLayout.razor.css new file mode 100644 index 0000000..dbc4235 --- /dev/null +++ b/src/NexusReader.UI.Shared/Layout/AuthLayout.razor.css @@ -0,0 +1,15 @@ +.nexus-auth-shell { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: radial-gradient(circle at center, #1b202e 0%, #0f1115 100%) !important; + display: flex; + justify-content: center; + align-items: center; + z-index: 99999; + margin: 0; + padding: 0; + overflow: hidden; +} diff --git a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor index 0b21a66..6511897 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor +++ b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor @@ -89,7 +89,7 @@ Node: @_profile.TenantId.ToString().ToUpper()
- + +
+ } + else if (!BookId.HasValue || BookId.Value == Guid.Empty || Nodes == null || !Nodes.Any()) + { +
+ +

Brak Aktywnych Książek

+

Nie wybrano żadnej książki lub ta książka nie ma jeszcze wygenerowanej mapy pojęć przez Nexus AI.

+ Przejdź do Biblioteki +
+ } + else + { +
+
+ +
+
+

Mapa Pojęć

+ Interaktywna ścieżka rozwoju Twoich postępów nauki +
+
+ +
+
+ +
+ +
+
+

Ścieżka Rozwoju Wiedzy

+
+
+ +
+
+ + +
+ @if (SelectedNode == null) + { +
+
+ +
+

Wybierz węzeł na mapie

+

Kliknij dowolne pojęcie z lewego panelu, aby uruchomić głęboką analizę i prześledzić szczegóły wygenerowane przez sztuczną inteligencję.

+
+ } + else + { + var isSelectedNodeUnlocked = IsUnlocked(SelectedNode.Id); + +
+
+
+ @SelectedNode.Id.ToUpper() + @if (isSelectedNodeUnlocked) + { + + Odblokowane + + } + else + { + + Zablokowane + + } +
+

@SelectedNode.Label

+
+ +
+ @if (!isSelectedNodeUnlocked) + { +
+ +
+ Ten etap jest zablokowany +

Kontynuuj czytanie książki, aby odblokować to pojęcie. Po przeczytaniu rozdziału, postęp zsynchronizuje się automatycznie.

+
+
+ } + + + + + + @if (SelectedNode.KeyTerms != null && SelectedNode.KeyTerms.Any()) + { + + } +
+ + +
+ } +
+
+ } + + +@code { + [Parameter] public Guid? BookId { get; set; } + + private List Nodes { get; set; } = new(); + private string LastReadBlockId { get; set; } = string.Empty; + private GraphNodeDto? SelectedNode { get; set; } + private bool _isLoading = true; + private string _errorMessage = string.Empty; + + protected override async Task OnInitializedAsync() + { + IdentityService.OnStateInvalidated += HandleStateInvalidatedAsync; + await SyncService.InitializeAsync(); + SyncService.OnProgressReceived += HandleProgressReceivedAsync; + await LoadDataAsync(); + } + + private async Task LoadDataAsync() + { + _isLoading = true; + _errorMessage = string.Empty; + StateHasChanged(); + + try + { + if (!BookId.HasValue || BookId.Value == Guid.Empty) + { + var profileResult = await IdentityService.GetProfileAsync(); + if (profileResult.IsSuccess && profileResult.Value.LastReadBook != null) + { + BookId = profileResult.Value.LastReadBook.Id; + } + } + + if (BookId.HasValue && BookId.Value != Guid.Empty) + { + var result = await ConceptsMapService.GetConceptsMapAsync(BookId.Value); + if (result.IsSuccess) + { + Nodes = result.Value.Nodes; + LastReadBlockId = result.Value.LastReadBlockId; + + if (Nodes.Any()) + { + SelectedNode = Nodes.FirstOrDefault(n => IsUnlocked(n.Id)) ?? Nodes.First(); + } + } + else + { + _errorMessage = result.Errors.FirstOrDefault()?.Message ?? "Brak odpowiedzi od serwera."; + } + } + } + catch (Exception ex) + { + _errorMessage = $"Błąd podczas pobierania danych: {ex.Message}"; + } + finally + { + _isLoading = false; + StateHasChanged(); + } + } + + private bool IsUnlocked(string nodeId) + { + if (string.IsNullOrEmpty(nodeId)) return false; + var nodeSeq = SegmentIdParser.Parse(nodeId); + + var minNodeSeq = Nodes.Any() ? Nodes.Min(n => SegmentIdParser.Parse(n.Id)) : 0; + if (nodeSeq == minNodeSeq) return true; + + if (string.IsNullOrEmpty(LastReadBlockId)) return false; + + var progressSeq = SegmentIdParser.Parse(LastReadBlockId); + return nodeSeq <= progressSeq; + } + + + + private void HandleNodeSelected(GraphNodeDto node) + { + SelectedNode = node; + StateHasChanged(); + } + + private void GoBackToLibrary() + { + NavigationManager.NavigateTo("/library"); + } + + private void GoToReader() + { + if (BookId.HasValue) + { + NavigationManager.NavigateTo($"/reader/{BookId.Value}"); + } + } + + private void GoToSelectedChapter() + { + if (BookId.HasValue && SelectedNode != null) + { + var chapterIndex = SegmentIdParser.Parse(SelectedNode.Id); + NavigationManager.NavigateTo($"/reader/{BookId.Value}?chapter={chapterIndex}"); + } + } + + private async Task HandleStateInvalidatedAsync() + { + await InvokeAsync(async () => + { + await LoadDataAsync(); + }); + } + + private async Task HandleProgressReceivedAsync(string pageId, DateTime timestamp) + { + await InvokeAsync(() => + { + LastReadBlockId = pageId; + StateHasChanged(); + return Task.CompletedTask; + }); + } + + public ValueTask DisposeAsync() + { + IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync; + SyncService.OnProgressReceived -= HandleProgressReceivedAsync; + return ValueTask.CompletedTask; + } +} diff --git a/src/NexusReader.UI.Shared/Pages/ConceptsDashboard.razor.css b/src/NexusReader.UI.Shared/Pages/ConceptsDashboard.razor.css new file mode 100644 index 0000000..53edb26 --- /dev/null +++ b/src/NexusReader.UI.Shared/Pages/ConceptsDashboard.razor.css @@ -0,0 +1,405 @@ +.concepts-dashboard-container { + width: 100%; + max-width: 1400px; + margin: 0 auto; + padding: 2rem 1.5rem; + box-sizing: border-box; + display: flex; + flex-direction: column; + gap: 1.5rem; + min-height: calc(100vh - 80px); +} + +/* Header Section */ +.dashboard-header { + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + gap: 2rem; + padding: 1.25rem 2rem; + background: rgba(20, 20, 20, 0.35); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 16px; + backdrop-filter: blur(12px); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2); +} + +.header-back .btn-back { + padding: 0.5rem 1.25rem; + font-size: 0.85rem; +} + +.header-back .btn-back:hover { + border-color: var(--nexus-neon); + color: var(--nexus-neon); + background: var(--nexus-primary-glow); + box-shadow: 0 0 10px var(--nexus-primary-glow); +} + +.header-title h1 { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + color: #fff; +} + +.header-title .subtitle { + font-size: 0.85rem; + color: rgba(255, 255, 255, 0.4); +} + +.header-actions .btn-action { + padding: 0.6rem 1.4rem; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.header-actions .btn-action:hover { + box-shadow: 0 0 20px var(--nexus-primary-glow); +} + +/* Grid Layout */ +.concepts-dashboard { + display: grid; + grid-template-columns: 1.1fr 1fr; + gap: 1.5rem; + height: 72vh; +} + +/* Glass Panels */ +.glass-panel { + display: flex; + flex-direction: column; + overflow: hidden; + padding: 0; +} + +.pane-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.pane-header h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #fff; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.pane-content { + flex-grow: 1; + overflow: hidden; +} + +/* Loading, Error and Empty States */ +.loading-state, .error-state, .empty-dashboard-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + margin: auto; + width: 100%; + max-width: 480px; + box-sizing: border-box; +} + +.preloader-robot { + position: relative; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.5rem; +} + +.neon-pulse { + color: var(--nexus-neon); + filter: drop-shadow(0 0 10px var(--nexus-neon)); + animation: robot-pulse 2s infinite ease-in-out; +} + +@keyframes robot-pulse { + 0% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } + 50% { transform: scale(1.08); filter: drop-shadow(0 0 25px var(--nexus-neon)); } + 100% { transform: scale(1); filter: drop-shadow(0 0 10px var(--nexus-neon)); } +} + +.scan-line { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 2px; + background: var(--nexus-neon); + box-shadow: 0 0 15px var(--nexus-neon); + animation: scan 2s infinite linear; + opacity: 0.8; +} + +@keyframes scan { + 0% { top: 0; } + 50% { top: 100%; } + 100% { top: 0; } +} + +.loading-text { + font-size: 0.95rem; + color: rgba(255, 255, 255, 0.7); + margin-top: 1rem; + letter-spacing: 0.05em; +} + +.error-icon, .dim-icon { + margin-bottom: 1.5rem; +} + +.error-icon { + color: #ff4a4a; + filter: drop-shadow(0 0 8px rgba(255, 74, 74, 0.4)); +} + +.dim-icon { + color: rgba(255, 255, 255, 0.15); +} + +.empty-dashboard-state h2, .error-state h3 { + color: #fff; + margin: 0 0 0.75rem 0; + font-weight: 600; +} + +.empty-dashboard-state p, .error-state p { + color: rgba(255, 255, 255, 0.45); + font-size: 0.88rem; + line-height: 1.5; + margin: 0 0 2rem 0; +} + +/* Workspace Panels */ +.workspace-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-grow: 1; + padding: 3rem; + text-align: center; + color: rgba(255, 255, 255, 0.4); +} + +.empty-glowing-brain { + width: 80px; + height: 80px; + border-radius: 50%; + background: rgba(0, 255, 153, 0.04); + border: 1px solid rgba(0, 255, 153, 0.15); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.5rem; + box-shadow: 0 0 20px var(--nexus-primary-glow); +} + +.workspace-empty h4 { + margin: 0 0 0.75rem 0; + color: #fff; + font-size: 1.1rem; + font-weight: 600; +} + +.workspace-empty p { + font-size: 0.85rem; + line-height: 1.5; + max-width: 320px; + margin: 0; +} + +.workspace-content { + display: flex; + flex-direction: column; + height: 100%; +} + +.workspace-header { + padding: 1.5rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.node-meta { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; +} + +.node-id { + font-family: var(--nexus-font-sans); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + color: var(--nexus-neon); +} + +.badge { + font-size: 0.7rem; + padding: 0.2rem 0.55rem; + border-radius: 12px; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.badge-unlocked { + background: rgba(0, 255, 153, 0.08); + color: var(--nexus-neon); + border: 1px solid rgba(0, 255, 153, 0.2); +} + +.badge-locked { + background: rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.4); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +.workspace-title { + margin: 0; + font-size: 1.4rem; + font-weight: 700; + color: #fff; +} + +.workspace-body { + flex-grow: 1; + overflow-y: auto; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +/* Scrollbar customization for workspace body */ +.workspace-body::-webkit-scrollbar { + width: 6px; +} +.workspace-body::-webkit-scrollbar-track { + background: transparent; +} +.workspace-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.08); + border-radius: 3px; +} +.workspace-body::-webkit-scrollbar-thumb:hover { + background: var(--nexus-neon); +} + +.locked-warning { + display: flex; + flex-direction: row; + gap: 1rem; + background: rgba(255, 171, 0, 0.04); + border: 1px solid rgba(255, 171, 0, 0.15); + border-radius: 8px; + padding: 1rem; + color: rgba(255, 255, 255, 0.85); +} + +.lock-warning-icon { + color: #ffab00; + flex-shrink: 0; + margin-top: 0.1rem; +} + +.locked-warning strong { + font-size: 0.85rem; + color: #ffab00; + display: block; + margin-bottom: 0.25rem; +} + +.locked-warning p { + margin: 0; + font-size: 0.8rem; + line-height: 1.4; + color: rgba(255, 255, 255, 0.55); +} + +.metadata-section h4 { + margin: 0 0 0.5rem 0; + font-size: 0.85rem; + font-weight: 600; + color: #aaa; + display: flex; + align-items: center; + gap: 0.35rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.section-text { + margin: 0; + font-size: 0.88rem; + line-height: 1.6; + color: rgba(255, 255, 255, 0.7); +} + +.summary-box { + background: rgba(255, 255, 255, 0.02); + border-left: 3px solid var(--nexus-neon); + border-radius: 0 8px 8px 0; + padding: 1rem; + margin-top: 0.25rem; +} + +.key-terms-grid { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.term-pill { + font-size: 0.75rem; + background: rgba(255, 255, 255, 0.03); + border: 1px solid rgba(255, 255, 255, 0.05); + color: rgba(255, 255, 255, 0.6); + padding: 0.3rem 0.75rem; + border-radius: 20px; + font-weight: 500; + transition: all 0.2s ease; +} + +.term-pill:hover { + border-color: rgba(0, 255, 153, 0.2); + color: var(--nexus-neon); + background: rgba(0, 255, 153, 0.03); +} + +.workspace-footer { + padding: 1.25rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +@media (max-width: 1024px) { + .concepts-dashboard { + grid-template-columns: 1fr; + height: auto; + } + .concepts-dashboard-container { + padding: 1rem; + } + .dashboard-header { + grid-template-columns: 1fr; + text-align: center; + gap: 1rem; + } + .header-back, .header-actions { + display: flex; + justify-content: center; + } +} diff --git a/src/NexusReader.UI.Shared/Pages/Intelligence.razor b/src/NexusReader.UI.Shared/Pages/Intelligence.razor index c8ea621..13404ff 100644 --- a/src/NexusReader.UI.Shared/Pages/Intelligence.razor +++ b/src/NexusReader.UI.Shared/Pages/Intelligence.razor @@ -120,7 +120,7 @@ @bind:event="oninput" @onkeyup="HandleKeyUp" disabled="@_isLoading" /> - @@ -106,403 +106,6 @@ - - @code { private bool _isModalOpen; private bool _isLoading = true; diff --git a/src/NexusReader.UI.Shared/Pages/Library.razor.css b/src/NexusReader.UI.Shared/Pages/Library.razor.css new file mode 100644 index 0000000..3fde1ce --- /dev/null +++ b/src/NexusReader.UI.Shared/Pages/Library.razor.css @@ -0,0 +1,368 @@ +.library-page { + padding: 3rem 2rem; + max-width: 1200px; + margin: 0 auto; + animation: fadeIn 0.6s ease-out; +} + +.library-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 3rem; + flex-wrap: wrap; + gap: 1.5rem; +} + +.header-title-section h1 { + font-family: var(--nexus-font-serif); + font-size: 2.8rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + background: linear-gradient(135deg, var(--nexus-text) 0%, rgba(255, 255, 255, 0.7) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -0.5px; +} + +.header-title-section .subtitle { + font-size: 1rem; + color: rgba(255, 255, 255, 0.6); + margin: 0; +} + +.add-book-trigger { + background: var(--nexus-neon) !important; + color: #000000 !important; + border: none !important; + box-shadow: 0 4px 15px var(--nexus-primary-glow) !important; + font-weight: 600 !important; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; + border-radius: var(--radius-md) !important; +} + +.add-book-trigger:hover { + transform: translateY(-2px) !important; + box-shadow: 0 8px 20px rgba(0, 255, 153, 0.5) !important; + filter: brightness(1.1); +} + +.btn-icon { + margin-right: 0.5rem; + font-weight: bold; +} + +/* Books Grid */ +.books-grid, .loading-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 2rem; +} + +.book-card { + cursor: pointer; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + border-radius: var(--radius-lg); + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.book-card::before { + content: ''; + position: absolute; + inset: 0; + background: radial-gradient(800px circle at var(--x, 0) var(--y, 0), rgba(255, 255, 255, 0.06), transparent 40%); + opacity: 0; + transition: opacity 0.5s; + pointer-events: none; +} + +.book-card:hover { + transform: translateY(-8px) scale(1.02); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3), 0 0 2px rgba(255, 255, 255, 0.1) inset; + border-color: rgba(0, 255, 153, 0.2); +} + +.book-card:hover::before { + opacity: 1; +} + +.book-cover-container { + position: relative; + height: 380px; + background: rgba(0, 0, 0, 0.2); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.book-cover { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.book-card:hover .book-cover { + transform: scale(1.08); +} + +.cover-overlay { + position: absolute; + inset: 0; + background: rgba(15, 23, 42, 0.6); + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 0.3s ease; + backdrop-filter: blur(4px); +} + +.book-card:hover .cover-overlay { + opacity: 1; +} + +.read-action { + color: #ffffff; + font-weight: 600; + font-size: 1.1rem; + padding: 0.75rem 1.5rem; + border: 2px solid #ffffff; + border-radius: 30px; + transform: translateY(10px); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.book-card:hover .read-action { + transform: translateY(0); + background: #ffffff; + color: #0f172a; +} + +.book-details { + padding: 1.5rem; + display: flex; + flex-direction: column; + flex-grow: 1; + background: rgba(15, 23, 42, 0.3); +} + +.book-title { + font-size: 1.25rem; + font-weight: 600; + margin: 0 0 0.5rem 0; + color: var(--nexus-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--nexus-font-sans); +} + +.book-author { + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.5); + margin: 0 0 1rem 0; +} + +.new-badge { + align-self: flex-start; + font-size: 0.75rem; + font-weight: 600; + color: var(--nexus-primary); + background: rgba(0, 255, 153, 0.15); + padding: 0.25rem 0.75rem; + border-radius: 20px; + border: 1px solid rgba(0, 255, 153, 0.3); +} + +/* Book Progress Bar */ +.book-progress-section { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.progress-bar { + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--nexus-neon) 0%, #00ccff 100%); + border-radius: 3px; +} + +.progress-text { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.4); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Empty State */ +.empty-state-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 5rem 2rem; + text-align: center; + border-radius: var(--radius-lg); +} + +.empty-icon-pulse { + margin-bottom: 2rem; + color: rgba(255, 255, 255, 0.2); + animation: pulse 3s infinite alternate; +} + +.empty-state-container h3 { + font-family: var(--nexus-font-serif); + font-size: 1.8rem; + margin: 0 0 0.5rem 0; + color: var(--nexus-text); +} + +.empty-state-container p { + color: rgba(255, 255, 255, 0.5); + max-width: 400px; + margin: 0 0 2rem 0; +} + +.restricted-info { + font-size: 0.85rem; + font-style: italic; + color: rgba(255, 255, 255, 0.35) !important; +} + +/* Skeleton Loading */ +.skeleton-card { + border-radius: var(--radius-lg); + overflow: hidden; + height: 480px; + background: rgba(255, 255, 255, 0.02) !important; + border: 1px solid rgba(255, 255, 255, 0.05) !important; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15) !important; + opacity: 0.5; +} + +.skeleton-cover { + height: 380px; + background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.04) 75%) !important; + background-size: 200% 100%; + animation: loading 1.5s infinite; +} + +.skeleton-details { + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.skeleton-line { + background: linear-gradient(90deg, rgba(255,255,255,0.04) 25%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.04) 75%) !important; + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: 4px; +} + +.skeleton-line.title { + height: 20px; + width: 80%; +} + +.skeleton-line.author { + height: 14px; + width: 50%; +} + +.skeleton-line.progress { + height: 8px; + width: 100%; + margin-top: auto; +} + +.library-loading-container { + position: relative; + width: 100%; +} + +.library-loading-container .loader-card { + position: absolute; + top: 180px; + left: 50%; + transform: translate(-50%, -50%); + z-index: 10; + display: flex; + align-items: center; + gap: 1.25rem; + padding: 1.25rem 2.25rem; + border-radius: 40px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4), 0 0 1px rgba(255, 255, 255, 0.15) inset; + background: rgba(15, 23, 42, 0.75); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.08); + animation: scaleIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.spinner-glow { + width: 60px; + height: 60px; + border: 3px solid rgba(0, 255, 153, 0.1); + border-radius: 50%; + border-top-color: var(--nexus-neon); + animation: spin 1s cubic-bezier(0.55, 0.055, 0.675, 0.19) infinite; + box-shadow: 0 0 15px rgba(0, 255, 153, 0.2); +} + +.spinner-glow.small { + width: 28px; + height: 28px; + border-width: 2px; +} + +.loader-text { + font-family: var(--nexus-font-sans); + font-weight: 500; + color: #ffffff; + font-size: 0.95rem; + letter-spacing: 0.2px; +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(15px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes pulse { + 0% { transform: scale(0.95); opacity: 0.6; } + 100% { transform: scale(1.05); opacity: 0.9; } +} + +@keyframes loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes scaleIn { + from { transform: translate(-50%, -50%) scale(0.9); opacity: 0; } + to { transform: translate(-50%, -50%) scale(1); opacity: 1; } +} diff --git a/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor b/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor index 08c8fde..2c6159c 100644 --- a/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor +++ b/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor @@ -2,13 +2,14 @@ @inject ILogger Logger @inject IJSRuntime JSRuntime +#if DEBUG
-
+

Serilog Logging Infrastructure

-

Production-grade diagnostic pipeline for unified native & web logs

+

Production-grade diagnostic pipeline for unified native & web logs

@@ -19,7 +20,7 @@
-
+

Native .NET Logs (C#)

@@ -42,7 +43,7 @@
-
+

Blazor / JS WebView Logs

@@ -62,7 +63,7 @@
-
+

Pipeline Diagnostics

@@ -87,255 +88,18 @@
+#else +
+
+

Diagnostics Unavailable

+

This page is only available in DEBUG builds.

+
+
+#endif - @code { +#if DEBUG private void LogInfo() { Logger.LogInformation("Structured native log triggered by user from SerilogDemo. Button: LogInfo"); @@ -367,4 +131,5 @@ { await JSRuntime.InvokeVoidAsync("eval", "throw new Error('Simulated runtime JS Exception triggered from Blazor UI button click!');"); } +#endif } diff --git a/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor.css b/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor.css new file mode 100644 index 0000000..f8c402a --- /dev/null +++ b/src/NexusReader.UI.Shared/Pages/SerilogDemo.razor.css @@ -0,0 +1,246 @@ +.serilog-demo-container { + padding: 3rem 2rem; + max-width: 1200px; + margin: 0 auto; + animation: fadeIn 0.6s ease-out; +} + +.header-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 2rem; + margin-bottom: 2rem; +} + +.header-content { + display: flex; + align-items: center; + gap: 1.5rem; +} + +::deep .header-icon { + color: var(--nexus-neon); + filter: drop-shadow(0 0 8px var(--nexus-primary-glow)); +} + +.header-text h1 { + font-family: var(--nexus-font-sans); + font-size: 1.8rem; + font-weight: 700; + margin: 0; + background: linear-gradient(to right, #ffffff, rgba(255, 255, 255, 0.7)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.subtitle { + margin: 0.25rem 0 0 0; + color: rgba(255, 255, 255, 0.6); + font-size: 0.95rem; +} + +.status-badge { + display: flex; + align-items: center; + gap: 0.5rem; + background: rgba(0, 255, 153, 0.05); + border: 1px solid rgba(0, 255, 153, 0.2); + padding: 0.5rem 1rem; + border-radius: 9999px; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.status-dot.green { + background-color: var(--nexus-neon); + box-shadow: 0 0 8px var(--nexus-neon); +} + +.status-text { + font-size: 0.85rem; + font-weight: 600; + color: var(--nexus-neon); +} + +.demo-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; + margin-bottom: 2rem; +} + +@media (max-width: 768px) { + .demo-grid { + grid-template-columns: 1fr; + } +} + +.control-card { + border-radius: var(--radius-lg); + padding: 2rem; + transition: all 0.3s ease; +} + +.card-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +::deep .card-icon { + color: var(--nexus-neon); +} + +::deep .js-icon { + color: #eab308; +} + +.card-header h2 { + font-size: 1.25rem; + font-weight: 600; + margin: 0; + color: #ffffff; +} + +.card-desc { + color: rgba(255, 255, 255, 0.5); + font-size: 0.9rem; + margin-bottom: 1.5rem; + line-height: 1.5; +} + +.btn-group { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1.25rem; + border-radius: var(--radius-md); + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + border: none; +} + +.btn-info { + background-color: rgba(0, 255, 153, 0.05); + color: var(--nexus-neon); + border: 1px solid rgba(0, 255, 153, 0.2) !important; +} + +.btn-info:hover { + background-color: var(--nexus-neon); + color: #000000; + box-shadow: 0 0 15px var(--nexus-primary-glow); +} + +.btn-warning { + background-color: rgba(245, 158, 11, 0.05); + color: #fbbf24; + border: 1px solid rgba(245, 158, 11, 0.2) !important; +} + +.btn-warning:hover { + background-color: #f59e0b; + color: #000000; + box-shadow: 0 0 15px rgba(245, 158, 11, 0.3); +} + +.btn-error { + background-color: rgba(239, 68, 68, 0.05); + color: #f87171; + border: 1px solid rgba(239, 68, 68, 0.2) !important; +} + +.btn-error:hover { + background-color: #ef4444; + color: white; + box-shadow: 0 0 15px rgba(239, 68, 68, 0.3); +} + +.btn-js-info { + background-color: rgba(234, 179, 8, 0.05); + color: #fef08a; + border: 1px solid rgba(234, 179, 8, 0.2) !important; +} + +.btn-js-info:hover { + background-color: #eab308; + color: #0f172a; + box-shadow: 0 0 15px rgba(234, 179, 8, 0.3); +} + +.btn-js-error { + background-color: rgba(236, 72, 153, 0.05); + color: #fbcfe8; + border: 1px solid rgba(236, 72, 153, 0.2) !important; +} + +.btn-js-error:hover { + background-color: #ec4899; + color: white; + box-shadow: 0 0 15px rgba(236, 72, 153, 0.3); +} + +.config-card { + border-radius: var(--radius-lg); + padding: 2rem; +} + +.config-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; + margin-top: 1.5rem; +} + +@media (max-width: 768px) { + .config-grid { + grid-template-columns: 1fr; + } +} + +.config-item { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.config-item .label { + font-size: 0.8rem; + color: rgba(255, 255, 255, 0.4); + text-transform: uppercase; + letter-spacing: 0.05em; + font-weight: 600; +} + +.config-item .value { + font-size: 0.95rem; + color: #cbd5e1; +} + +.code-value { + font-family: var(--nexus-font-mono); + background: rgba(0, 0, 0, 0.2); + padding: 0.25rem 0.5rem; + border-radius: 4px; + border: 1px solid rgba(255, 255, 255, 0.05); + word-break: break-all; +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(15px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/src/NexusReader.UI.Shared/Pages/Settings.razor b/src/NexusReader.UI.Shared/Pages/Settings.razor index f19734b..ae288b8 100644 --- a/src/NexusReader.UI.Shared/Pages/Settings.razor +++ b/src/NexusReader.UI.Shared/Pages/Settings.razor @@ -5,60 +5,15 @@

Settings

Configure your account and application preferences.

-
-

Diagnostics & System Logs

+ #if DEBUG +
+

Diagnostics & System Logs

Inspect native logging infrastructure, trigger custom logs, and trace WebView errors.

Open Serilog Diagnostics Dashboard
+ #endif
- diff --git a/src/NexusReader.UI.Shared/Pages/Settings.razor.css b/src/NexusReader.UI.Shared/Pages/Settings.razor.css new file mode 100644 index 0000000..7df77b0 --- /dev/null +++ b/src/NexusReader.UI.Shared/Pages/Settings.razor.css @@ -0,0 +1,74 @@ +.settings-page { + padding: 3rem 2rem; + max-width: 1200px; + margin: 0 auto; + animation: fadeIn 0.6s ease-out; +} + +.settings-page > h1 { + font-family: var(--nexus-font-serif); + font-size: 2.8rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + background: linear-gradient(135deg, var(--nexus-text) 0%, rgba(255, 255, 255, 0.7) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + letter-spacing: -0.5px; +} + +.settings-page > p { + font-size: 1rem; + color: rgba(255, 255, 255, 0.6); + margin-bottom: 3rem; +} + +.settings-section { + padding: 2rem; + margin-top: 1.5rem; + border-radius: var(--radius-lg); + transition: all 0.3s ease; +} + +.settings-section h2 { + font-family: var(--nexus-font-sans); + font-size: 1.35rem; + font-weight: 600; + color: #ffffff; + margin: 0 0 0.75rem 0; +} + +.settings-section p { + color: rgba(255, 255, 255, 0.5); + font-size: 0.95rem; + margin: 0 0 1.5rem 0; + line-height: 1.5; +} + +.diag-btn { + display: inline-flex; + align-items: center; + gap: 0.5rem; + background: rgba(0, 255, 153, 0.05); + color: var(--nexus-neon); + border: 1px solid rgba(0, 255, 153, 0.2); + padding: 0.75rem 1.5rem; + border-radius: var(--radius-md); + text-decoration: none; + font-size: 0.9rem; + font-weight: 600; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 0 0 transparent; +} + +.diag-btn:hover { + background: var(--nexus-neon); + color: #000000; + border-color: var(--nexus-neon); + box-shadow: 0 0 15px var(--nexus-primary-glow); + transform: translateY(-2px); +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(15px); } + to { opacity: 1; transform: translateY(0); } +} diff --git a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs index 9489616..1986436 100644 --- a/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs +++ b/src/NexusReader.UI.Shared/Services/KnowledgeCoordinator.cs @@ -75,7 +75,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable } } - public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global") + public async Task ProcessFullPageAsync(string fullContent, string tenantId = "global", Guid? ebookId = null) { if (string.IsNullOrWhiteSpace(fullContent)) return; @@ -87,7 +87,7 @@ public sealed partial class KnowledgeCoordinator : IDisposable try { - var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId); + var result = await _knowledgeService.GetGraphDataAsync(fullContent, tenantId, ebookId); if (result.IsSuccess) { var packet = result.Value; diff --git a/src/NexusReader.UI.Shared/wwwroot/app.css b/src/NexusReader.UI.Shared/wwwroot/app.css index 8de4576..2238d4d 100644 --- a/src/NexusReader.UI.Shared/wwwroot/app.css +++ b/src/NexusReader.UI.Shared/wwwroot/app.css @@ -2,6 +2,7 @@ :root { --nexus-neon: #00ff99; + --nexus-neon-glow: rgba(0, 255, 153, 0.3); --nexus-bg: #121212; --nexus-card: #1a1a1a; --nexus-text: #ffffff; @@ -9,6 +10,17 @@ --nexus-font-sans: 'Inter', sans-serif; --nexus-font-serif: 'Merriweather', serif; + /* Global Semantic Theme Mapping */ + --nexus-primary: var(--nexus-neon); + --nexus-primary-glow: var(--nexus-neon-glow); + --nexus-primary-hover: #00e688; + + /* Standard Layout Tokens */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-xl: 20px; + /* Safe Area Insets with fallbacks */ --safe-area-inset-top: env(safe-area-inset-top, 0px); --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); @@ -23,7 +35,7 @@ .glass-panel { background: rgba(20, 20, 20, 0.85); /* Darker fallback for readability */ border: 1px solid rgba(255, 255, 255, 0.05); - border-radius: 20px; + border-radius: var(--radius-xl); padding: 1.5rem; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } @@ -35,6 +47,40 @@ } } +/* Unified Enterprise Component Constraints */ +.btn-nexus { + font-family: var(--nexus-font-sans); + font-weight: 600; + border-radius: var(--radius-md); + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + border: none; + text-decoration: none; +} +.btn-nexus-primary { + background: var(--nexus-neon); + color: #000000; +} +.btn-nexus-secondary { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: #ffffff; +} +.btn-nexus:hover { + transform: translateY(-2px); + filter: brightness(1.1); +} +.btn-nexus-primary:hover { + box-shadow: 0 4px 15px var(--nexus-primary-glow); +} +.btn-nexus-secondary:hover { + box-shadow: 0 4px 15px rgba(255, 255, 255, 0.05); +} + .theme-light { --nexus-bg: var(--nexus-paper); diff --git a/src/NexusReader.UI.Shared/wwwroot/css/nexus-auth.css b/src/NexusReader.UI.Shared/wwwroot/css/nexus-auth.css index 01d40a1..ab7b525 100644 --- a/src/NexusReader.UI.Shared/wwwroot/css/nexus-auth.css +++ b/src/NexusReader.UI.Shared/wwwroot/css/nexus-auth.css @@ -1,9 +1,8 @@ :root { - --nexus-primary: #44ff77; - --nexus-bg: #121418; - --nexus-card-bg: #1c1f24; + --nexus-primary: var(--nexus-neon); + --nexus-card-bg: var(--nexus-card); --nexus-border: rgba(255, 255, 255, 0.08); - --nexus-text-muted: #666; + --nexus-text-muted: #888888; --nexus-text-bright: #e2e8f0; } diff --git a/src/NexusReader.Web.Client/Program.cs b/src/NexusReader.Web.Client/Program.cs index 9eed57e..a011bc3 100644 --- a/src/NexusReader.Web.Client/Program.cs +++ b/src/NexusReader.Web.Client/Program.cs @@ -36,6 +36,7 @@ builder.Services.AddCascadingAuthenticationState(); // AI & Content Services builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddTransient(); builder.Services.AddHttpClient("NexusAPI", client => diff --git a/src/NexusReader.Web.Client/Services/WasmConceptsMapService.cs b/src/NexusReader.Web.Client/Services/WasmConceptsMapService.cs new file mode 100644 index 0000000..41d10fa --- /dev/null +++ b/src/NexusReader.Web.Client/Services/WasmConceptsMapService.cs @@ -0,0 +1,36 @@ +using System.Net.Http.Json; +using FluentResults; +using NexusReader.Application.Abstractions.Services; +using NexusReader.Application.Queries.Concepts; + +namespace NexusReader.Web.Client.Services; + +public class WasmConceptsMapService : IConceptsMapService +{ + private readonly HttpClient _httpClient; + + public WasmConceptsMapService(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task> GetConceptsMapAsync(Guid bookId) + { + try + { + var response = await _httpClient.GetAsync($"api/book/{bookId}/concepts-map"); + if (response.IsSuccessStatusCode) + { + var result = await response.Content.ReadFromJsonAsync(); + return result != null ? Result.Ok(result) : Result.Fail("Błąd deserializacji mapy pojęć."); + } + + var errorContent = await response.Content.ReadAsStringAsync(); + return Result.Fail($"Błąd serwera ({response.StatusCode}): {errorContent}"); + } + catch (Exception ex) + { + return Result.Fail(new Error("Błąd sieci przy pobieraniu mapy pojęć.").CausedBy(ex)); + } + } +} diff --git a/src/NexusReader.Web/Program.cs b/src/NexusReader.Web/Program.cs index 9e10a59..70cdb68 100644 --- a/src/NexusReader.Web/Program.cs +++ b/src/NexusReader.Web/Program.cs @@ -7,6 +7,7 @@ using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Queries.User; using NexusReader.Application.Commands.Library; using NexusReader.Application.Queries.Library; +using NexusReader.Application.Queries.Concepts; using MediatR; using NexusReader.Web.Client.Services; using NexusReader.UI.Shared.Services; @@ -73,6 +74,7 @@ builder.Services.AddHttpClient("NexusAPI", (sp, client) => builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("NexusAPI")); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddApplication(); @@ -86,6 +88,11 @@ builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( // Authorization Policies builder.Services.AddScoped(); builder.Services.AddAuthorizationBuilder() + .SetDefaultPolicy(new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder( + IdentityConstants.ApplicationScheme, + IdentityConstants.BearerScheme) + .RequireAuthenticatedUser() + .Build()) .AddPolicy("ProUser", policy => policy.RequireClaim("Plan", SubscriptionPlan.ProName, SubscriptionPlan.EnterpriseName)) .AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement())); @@ -369,6 +376,28 @@ app.MapGet("/api/library/books", async (ClaimsPrincipal user, IMediator mediator return Results.BadRequest(errorMsg); }).RequireAuthorization(); +app.MapGet("/api/book/{bookId:guid}/concepts-map", async ( + Guid bookId, + ClaimsPrincipal user, + IMediator mediator) => +{ + var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); + var tenantId = user.FindFirstValue("TenantId") ?? "global"; + + if (string.IsNullOrEmpty(userId)) + { + return Results.Unauthorized(); + } + + var result = await mediator.Send(new GetBookConceptsMapQuery(bookId, userId, tenantId)); + if (result.IsFailed) + { + return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Failed to fetch concepts map."); + } + + return Results.Ok(result.Value); +}).RequireAuthorization(); + app.MapPost("/api/StripeWebhook", async ( HttpContext context, UserManager userManager, diff --git a/src/NexusReader.Web/Services/ServerConceptsMapService.cs b/src/NexusReader.Web/Services/ServerConceptsMapService.cs new file mode 100644 index 0000000..04845e4 --- /dev/null +++ b/src/NexusReader.Web/Services/ServerConceptsMapService.cs @@ -0,0 +1,49 @@ +using System.Security.Claims; +using FluentResults; +using MediatR; +using NexusReader.Application.Abstractions.Services; +using NexusReader.Application.Queries.Concepts; + +namespace NexusReader.Web.Services; + +public class ServerConceptsMapService : IConceptsMapService +{ + private readonly IMediator _mediator; + private readonly IIdentityService _identityService; + + public ServerConceptsMapService( + IMediator mediator, + IIdentityService identityService) + { + _mediator = mediator; + _identityService = identityService; + } + + public async Task> GetConceptsMapAsync(Guid bookId) + { + try + { + var profileResult = await _identityService.GetProfileAsync(); + + if (profileResult.IsFailed) + { + return Result.Fail("Użytkownik nie jest uwierzytelniony."); + } + + var profile = profileResult.Value; + var userId = profile.UserId; + var tenantId = profile.TenantId.ToString(); + + if (string.IsNullOrEmpty(userId)) + { + return Result.Fail("Nie znaleziono identyfikatora użytkownika."); + } + + return await _mediator.Send(new GetBookConceptsMapQuery(bookId, userId, tenantId)); + } + catch (Exception ex) + { + return Result.Fail(new Error("Błąd pobierania mapy pojęć na serwerze.").CausedBy(ex)); + } + } +} diff --git a/src/NexusReader.Web/Services/ServerIdentityService.cs b/src/NexusReader.Web/Services/ServerIdentityService.cs index 2a1aaff..56b0061 100644 --- a/src/NexusReader.Web/Services/ServerIdentityService.cs +++ b/src/NexusReader.Web/Services/ServerIdentityService.cs @@ -9,6 +9,7 @@ using NexusReader.Application.Queries.User; using MediatR; using NexusReader.Application.Constants; using NexusReader.Application.Abstractions.Services; +using Microsoft.AspNetCore.Components.Authorization; namespace NexusReader.Web.Services; @@ -19,6 +20,7 @@ public class ServerIdentityService : IIdentityService private readonly IHttpContextAccessor _httpContextAccessor; private readonly IMediator _mediator; private readonly INativeStorageService _storageService; + private readonly AuthenticationStateProvider _authStateProvider; public event Func? OnStateInvalidated; @@ -27,13 +29,15 @@ public class ServerIdentityService : IIdentityService SignInManager signInManager, IHttpContextAccessor httpContextAccessor, IMediator mediator, - INativeStorageService storageService) + INativeStorageService storageService, + AuthenticationStateProvider authStateProvider) { _userManager = userManager; _signInManager = signInManager; _httpContextAccessor = httpContextAccessor; _mediator = mediator; _storageService = storageService; + _authStateProvider = authStateProvider; } public async Task LoginAsync(string email, string password, bool rememberMe = false) @@ -107,8 +111,15 @@ public class ServerIdentityService : IIdentityService public async Task> GetProfileAsync() { - var user = _httpContextAccessor.HttpContext?.User; - if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated."); + var authState = await _authStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user == null || user.Identity?.IsAuthenticated != true) + { + user = _httpContextAccessor.HttpContext?.User; + } + + if (user == null || user.Identity?.IsAuthenticated != true) return Result.Fail("Not authenticated."); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Result.Fail("User ID not found.");