feat(ui): implement premium gamified Concepts Map dashboard, unify design tokens, and enforce scoped CSS (#54)
Resolves #55 # gamified Concepts Map Dashboard, Architecture Cleanup, & CSS Unification This Pull Request completes the gamified **Concepts Map** interactive experience and executes a strict visual and architectural clean-up of the **NexusReader SaaS** platform by consolidating styling around the centralized **Nexus Neon** design system, enforcing Native AOT-compliant scoped-CSS, and resolving style drift. --- ### 🚀 Key Implementations #### 1. Gamified Interactive Concepts Map Dashboard * **Dynamic Skill-Tree Visualizer:** Implemented a gamified node-based interactive tree that reads concept connections dynamically, rendering progress nodes, unlocked/locked states, and active visual connection lines. * **Premium UX Details:** Integrated high-fidelity hover effects, detailed sidebar popovers showing unlocked stats/summaries, and smooth parallax backdrops. * **Secure Multi-Tenant Gating:** Hardened data queries using explicit `TenantId` gating to ensure complete layout isolation between system tenants. #### 2. Architecture & Service Optimization * **Service Abstraction (`IConceptsMapService`):** Introduced a clean service interface decoupled from the UI layers. * **Polyglot Fallback Implementations:** - **WasmConceptsMapService:** Implements efficient client-side fetching with error recovery/loading states. - **ServerConceptsMapService:** Handles deep database graph mapping, relationship resolution, and multi-tenant checks. * **CQRS Integrity:** Enforced the CQRS Result Pattern by using `MediatR` handlers to fetch data structures instead of placing database queries in UI modules. #### 3. Nexus Neon CSS Unification & Zero-Style-Tag Standards * **Central Design Tokens:** Solidified typography (`Inter` / `Merriweather`), primary brand green hues (`var(--nexus-neon)` at `#00ff99`), and glow variables inside the global `app.css`. * **Zero Inline Style Tags:** Standardized all dashboard modules by completely eliminating inline `<style>` blocks in favor of scoped `.razor.css` companion files. * **Consolidated Buttons & Glass Panels:** - Standardized `.btn-nexus`, `.btn-nexus-primary`, and `.btn-nexus-secondary` throughout header/footer/card operations. - Removed duplicate `.glass-panel` background, blur, and support-declaration overrides, making components cleanly inherit standard global styles. * **Eliminated Brand Splitting:** Resolved legacy purple-indigo user avatar and conversation bubble gradients inside the AI Intelligence Hub (`Intelligence.razor.css`), migrating them to premium glassmorphic surfaces (`rgba(255, 255, 255, 0.05)`) contrasting beautifully against the emerald green AI theme. --- ### 🧪 Verification & Build Gate Status * **Successful Compilation Check:** Run `dotnet build NexusReader.slnx --no-restore` from the workspace root. Flawlessly succeeded with **zero compilation errors** (`Liczba błędów: 0`) under target `.NET 10`. * **Verified Skills Synchrony:** The companion design guidelines skill (`nexus-ui-engine/SKILL.md`) has been fully updated to secure these layout and styling conventions for future dashboard features. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #54 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #54.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
using NexusReader.Domain.Entities;
|
||||
|
||||
namespace NexusReader.Application.Abstractions.Persistence;
|
||||
|
||||
/// <summary>
|
||||
/// Read-only abstraction for fetching concepts map data.
|
||||
/// Defined in the Application layer to avoid a direct dependency on EF Core / NexusReader.Data.
|
||||
/// </summary>
|
||||
public interface IConceptsMapReadRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the last read page ID for the specified user.
|
||||
/// </summary>
|
||||
Task<string?> GetLastReadPageIdAsync(string userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all knowledge units associated with a book, scoped by tenant.
|
||||
/// </summary>
|
||||
Task<List<KnowledgeUnit>> GetKnowledgeUnitsForBookAsync(
|
||||
Guid bookId,
|
||||
string tenantId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using FluentResults;
|
||||
using NexusReader.Application.Queries.Concepts;
|
||||
|
||||
namespace NexusReader.Application.Abstractions.Services;
|
||||
|
||||
public interface IConceptsMapService
|
||||
{
|
||||
Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using NexusReader.Application.Queries.Graph;
|
||||
|
||||
namespace NexusReader.Application.Queries.Concepts;
|
||||
|
||||
public record BookConceptsMapResultDto(
|
||||
[property: JsonPropertyName("nodes")] List<GraphNodeDto> Nodes,
|
||||
[property: JsonPropertyName("lastReadBlockId")] string LastReadBlockId
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
|
||||
namespace NexusReader.Application.Queries.Concepts;
|
||||
|
||||
public record GetBookConceptsMapQuery(
|
||||
Guid BookId,
|
||||
string UserId,
|
||||
string TenantId
|
||||
) : IQuery<BookConceptsMapResultDto>;
|
||||
@@ -0,0 +1,92 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentResults;
|
||||
using NexusReader.Application.Abstractions.Messaging;
|
||||
using NexusReader.Application.Abstractions.Persistence;
|
||||
using NexusReader.Application.Queries.Graph;
|
||||
using NexusReader.Application.Utilities;
|
||||
|
||||
namespace NexusReader.Application.Queries.Concepts;
|
||||
|
||||
internal sealed class GetBookConceptsMapQueryHandler : IQueryHandler<GetBookConceptsMapQuery, BookConceptsMapResultDto>
|
||||
{
|
||||
private readonly IConceptsMapReadRepository _repository;
|
||||
|
||||
public GetBookConceptsMapQueryHandler(IConceptsMapReadRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
public async Task<Result<BookConceptsMapResultDto>> Handle(GetBookConceptsMapQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
// 1. Fetch user to extract reading progress (LastReadPageId)
|
||||
var lastReadPageId = await _repository.GetLastReadPageIdAsync(request.UserId, cancellationToken);
|
||||
var lastReadBlockId = lastReadPageId ?? string.Empty;
|
||||
|
||||
// 2. Fetch all KnowledgeUnits associated with the ebook and user's tenant
|
||||
var units = await _repository.GetKnowledgeUnitsForBookAsync(request.BookId, request.TenantId, cancellationToken);
|
||||
|
||||
var nodes = new List<GraphNodeDto>();
|
||||
|
||||
foreach (var unit in units)
|
||||
{
|
||||
// Only process units representing sections or conceptual milestones (usually starting with "seg-")
|
||||
if (string.IsNullOrEmpty(unit.Id) || !unit.Id.StartsWith("seg-"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string label = unit.Id;
|
||||
string group = "concept";
|
||||
string summary = unit.Content;
|
||||
var keyTerms = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(unit.MetadataJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(unit.MetadataJson);
|
||||
if (doc.RootElement.TryGetProperty("label", out var labelProp))
|
||||
label = labelProp.GetString() ?? label;
|
||||
if (doc.RootElement.TryGetProperty("group", out var groupProp))
|
||||
group = groupProp.GetString() ?? group;
|
||||
if (doc.RootElement.TryGetProperty("summary", out var summaryProp))
|
||||
summary = summaryProp.GetString() ?? summary;
|
||||
if (doc.RootElement.TryGetProperty("key_terms", out var ktProp) && ktProp.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var term in ktProp.EnumerateArray())
|
||||
{
|
||||
if (term.GetString() is string s)
|
||||
keyTerms.Add(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback to defaults
|
||||
}
|
||||
}
|
||||
|
||||
nodes.Add(new GraphNodeDto(
|
||||
Id: unit.Id,
|
||||
Label: label,
|
||||
Group: group,
|
||||
Description: unit.Content,
|
||||
Type: unit.Type.ToString(),
|
||||
Summary: summary,
|
||||
KeyTerms: keyTerms
|
||||
));
|
||||
}
|
||||
|
||||
// Return sorted by the numeric value in the seg-ID to ensure topdown vertical alignment
|
||||
var sortedNodes = nodes
|
||||
.OrderBy(n => SegmentIdParser.Parse(n.Id))
|
||||
.ToList();
|
||||
|
||||
return Result.Ok(new BookConceptsMapResultDto(sortedNodes, lastReadBlockId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace NexusReader.Application.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Shared utility for parsing numeric segment identifiers from IDs like "seg-42".
|
||||
/// Centralizes the parsing contract to avoid duplication across handlers and UI components.
|
||||
/// </summary>
|
||||
public static class SegmentIdParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Extracts the numeric portion from a segment identifier string (e.g., "seg-42" → 42).
|
||||
/// Returns 0 if the string is null, empty, or contains no digits.
|
||||
/// </summary>
|
||||
public static int Parse(string? id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id)) return 0;
|
||||
var digits = new string(id.Where(char.IsDigit).ToArray());
|
||||
return int.TryParse(digits, out var val) ? val : 0;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user